Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexsteinerde committed Feb 28, 2021
0 parents commit 8283d48
Show file tree
Hide file tree
Showing 29 changed files with 1,307 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
Package.resolved
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2021 Alexander Steiner

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
28 changes: 28 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "docker-client-swift",
platforms: [.macOS(.v10_15)],
products: [
.library(name: "DockerClient", targets: ["DockerClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"),
],
targets: [
.target(
name: "DockerClient",
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
]),
.testTarget(
name: "DockerClientTests",
dependencies: ["DockerClient"]
),
]
)
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Docker Client
[![Language](https://img.shields.io/badge/Swift-5.4-brightgreen.svg)](http://swift.org)
[![Docker Engine API](https://img.shields.io/badge/Docker%20Engine%20API-%20%201.4.1-blue)](https://docs.docker.com/engine/api/v1.41/)

This is a Docker Client written in Swift. It's using the NIO Framework to communicate with the Docker Engine via sockets.

## Current Use Cases
- [x] List of all images
- [x] List of all containers
- [x] Pull an image
- [x] Create a new container from an image
- [x] Start a container
- [x] Get the stdOut and stdErr output of a container
- [x] Get the docker version information


## Installation
```Swift
import PackageDescription

let package = Package(
dependencies: [
.package(url: "https://github.com/alexsteinerde/docker-client-swift.git", from: "0.1.0"),
],
targets: [
.target(name: "App", dependencies: ["DockerClient"]),
...
]
)
```

## Usage Example
```swift
let client = DockerClient()
let image = try client.images.pullImage(imageName: "hello-world:latest").wait()
let container = try! client.containers.createContainer(image: image).wait()
try container.start(on: client).wait()
let output = try container.logs(on: client).wait()
print(output)
```

For further usage examples, please consider looking at the provided test cases.

## Security Advice
When using this in production, make sure you secure your appclication so no others can execute code. Otherwise the attacker could access your Docker environment and so all of the containers running in it.

## License
This project is released under the MIT license. See [LICENSE](LICENSE) for details.

## Contribution
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 :)
66 changes: 66 additions & 0 deletions Sources/DockerClient/APIs/DockerClient+Container.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation
import NIO

extension DockerClient {

/// APIs related to containers.
public var containers: ContainersAPI {
ContainersAPI(client: self)
}

public struct ContainersAPI {
fileprivate var client: DockerClient

public func list(all: Bool=false) throws -> EventLoopFuture<[Container]> {
try client.run(ListContainersEndpoint(all: all))
.map({ containers in
containers.map { container in
Container(id: .init(container.Id), image: Image(id: .init(container.Image), digest: nil), createdAt: Date(timeIntervalSince1970: TimeInterval(container.Created)))
}
})
}

public func createContainer(image: Image, commands: [String]?=nil) throws -> EventLoopFuture<Container> {
var id = image.id.value
if let tag = image.repositoryTags.first?.tag {
id += ":\(tag)"
}
if let digest = image.digest {
id += "@\(digest.rawValue)"
}
return try client.run(CreateContainerEndpoint(imageName: id, commands: commands))
.map({ response in
Container(id: .init(response.Id), image: image, createdAt: .init())
})
}

public func start(container: Container) throws -> EventLoopFuture<Void> {
return try client.run(StartContainerEndpoint(containerId: container.id.value))
.map({ _ in Void() })
}

public func logs(container: Container) throws -> EventLoopFuture<String> {
try client.run(GetContainerLogsEndpoint(containerId: container.id.value))
.map({ response in
// Removing the first character of each line because random characters went there
response.split(separator: "\n")
.map({ originalLine in
var line = originalLine
line.removeFirst(8)
return String(line)
})
.joined(separator: "\n")
})
}
}
}

extension Container {
public func start(on client: DockerClient) throws -> EventLoopFuture<Void> {
try client.containers.start(container: self)
}

public func logs(on client: DockerClient) throws -> EventLoopFuture<String> {
try client.containers.logs(container: self)
}
}
37 changes: 37 additions & 0 deletions Sources/DockerClient/APIs/DockerClient+Image.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import NIO

extension DockerClient {

/// APIs related to images.
public var images: ImagesAPI {
ImagesAPI(client: self)
}

public struct ImagesAPI {
fileprivate var client: DockerClient

public func pullImage(byName name: Identifier<Image>, tag: String?=nil, digest: Digest?=nil) throws -> EventLoopFuture<Image> {
var identifier = name.value
if let tag = tag {
identifier += ":\(tag)"
}
if let digest = digest {
identifier += "@\(digest.rawValue)"
}
return try client.run(PullImageEndpoint(imageName: identifier))
.map({ response in
Image(id: name, digest: .init(response.digest), repoTags: tag.map({ ["\(name.value):\($0)"] }))
})
}

public func list(all: Bool=false) throws -> EventLoopFuture<[Image]> {
try client.run(ListImagesEndpoint(all: all))
.map({ images in
images.map { image in
Image(id: .init(image.Id), digest: image.RepoDigests?.first.map({ Digest.init($0) }), repoTags: image.RepoTags, createdAt: Date(timeIntervalSince1970: TimeInterval(image.Created)))
}
})
}
}
}
10 changes: 10 additions & 0 deletions Sources/DockerClient/APIs/DockerClient+System.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import NIO

extension DockerClient {
public func version() throws -> EventLoopFuture<DockerVersion> {
try run(VersionEndpoint())
.map({ response in
DockerVersion(version: response.version, architecture: response.arch, kernelVersion: response.kernelVersion, minAPIVersion: response.minAPIVersion, os: response.os)
})
}
}
35 changes: 35 additions & 0 deletions Sources/DockerClient/APIs/DockerClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import NIO
import NIOHTTP1
import AsyncHTTPClient
import Logging

/// The entry point for docker client commands.
public class DockerClient {
private let daemonSocket: String
private let client: HTTPClient
private let logger: Logger

public init(daemonSocket: String = "/var/run/docker.sock", client: HTTPClient = .init(eventLoopGroupProvider: .createNew), logger: Logger = .init(label: "docker-client")) {
self.daemonSocket = daemonSocket
self.client = client
self.logger = logger
}

deinit {
try? client.syncShutdown()
}

func run<T: Endpoint>(_ endpoint: T) throws -> EventLoopFuture<T.Response> {
logger.info("Execute: \(endpoint.path)")
return client.execute(endpoint.method, socketPath: daemonSocket, urlPath: "/v1.40/\(endpoint.path)", body: endpoint.body.map {HTTPClient.Body.data( try! $0.encode())}, logger: logger, headers: HTTPHeaders([("Content-Type", "application/json")]))
.logResponseBody(logger)
.decode(as: T.Response.self)
}

func run<T: PipelineEndpoint>(_ endpoint: T) throws -> EventLoopFuture<T.Response> {
client.execute(endpoint.method, socketPath: daemonSocket, urlPath: "/v1.40/\(endpoint.path)", body: try endpoint.body.map({ HTTPClient.Body.data( try $0.encode()) }))
.logResponseBody(logger)
.mapString(map: endpoint.map(data: ))
}
}
31 changes: 31 additions & 0 deletions Sources/DockerClient/Endpoints/CreateContainerEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import NIOHTTP1

struct CreateContainerEndpoint: Endpoint {
var body: CreateContainerBody?

typealias Response = CreateContainerResponse
typealias Body = CreateContainerBody
var method: HTTPMethod = .POST

private let imageName: String
private let commands: [String]?

init(imageName: String, commands: [String]?=nil) {
self.imageName = imageName
self.commands = commands
self.body = .init(Image: imageName, Cmd: commands)
}

var path: String {
"containers/create"
}

struct CreateContainerBody: Codable {
let Image: String
let Cmd: [String]?
}

struct CreateContainerResponse: Codable {
let Id: String
}
}
18 changes: 18 additions & 0 deletions Sources/DockerClient/Endpoints/GetContainerLogsEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import NIOHTTP1

struct GetContainerLogsEndpoint: Endpoint {
typealias Body = NoBody

typealias Response = String
var method: HTTPMethod = .GET

private let containerId: String

init(containerId: String) {
self.containerId = containerId
}

var path: String {
"containers/\(containerId)/logs?stdout=true&stderr=true"
}
}
29 changes: 29 additions & 0 deletions Sources/DockerClient/Endpoints/ListContainersEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import NIOHTTP1

struct ListContainersEndpoint: Endpoint {
typealias Body = NoBody
typealias Response = [ContainerResponse]
var method: HTTPMethod = .GET

private var all: Bool

init(all: Bool) {
self.all = all
}

var path: String {
"containers/json?all=\(all)"
}

struct ContainerResponse: Codable {
let Id: String
let Names: [String]
let Image: String
let ImageID: String
let Command: String
let Created: Int
let State: String
let Status: String
// TODO: Add additional fields
}
}
30 changes: 30 additions & 0 deletions Sources/DockerClient/Endpoints/ListImagesEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import NIOHTTP1

struct ListImagesEndpoint: Endpoint {
typealias Body = NoBody
typealias Response = [ImageResponse]
var method: HTTPMethod = .GET

private var all: Bool

init(all: Bool) {
self.all = all
}

var path: String {
"images/json?all=\(all)"
}

struct ImageResponse: Codable {
let Id: String
let ParentId: String
let RepoTags: [String]?
let RepoDigests: [String]?
let Created: Int
let Size: Int
let VirtualSize: Int
let SharedSize: Int
let Containers: Int
// TODO: Add additional fields
}
}
Loading

0 comments on commit 8283d48

Please sign in to comment.