├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── DockerClientSwift │ ├── APIs │ ├── DockerClient+Container.swift │ ├── DockerClient+Image.swift │ ├── DockerClient+Service.swift │ ├── DockerClient+System.swift │ └── DockerClient.swift │ ├── Endpoints │ ├── Containers │ │ ├── CreateContainerEndpoint.swift │ │ ├── GetContainerLogsEndpoint.swift │ │ ├── InspectContainerEndpoint.swift │ │ ├── ListContainersEndpoint.swift │ │ ├── PruneContainersEndpoint.swift │ │ ├── RemoveContainerEndpoint.swift │ │ ├── StartContainerEndpoint.swift │ │ └── StopContainerEndpoint.swift │ ├── Endpoint.swift │ ├── Images │ │ ├── InspectImageEndpoint.swift │ │ ├── ListImagesEndpoint.swift │ │ ├── PruneImagesEndpoint.swift │ │ ├── PullImageEndpoint.swift │ │ └── RemoveImageEndpoint.swift │ ├── Services │ │ ├── CreateServiceEndpoint.swift │ │ ├── InspectServiceEndpoint.swift │ │ ├── ListServicesEndpoint.swift │ │ └── UpdateServiceEndpoint.swift │ └── System │ │ ├── SystemInformationEndpoint.swift │ │ └── VersionEndpoint.swift │ ├── Helper │ ├── DateParsing.swift │ ├── EventLoopFuture+FlatMap.swift │ ├── HTTPClient+Codable.swift │ ├── HTTPClient+ExecuteOnSocket.swift │ └── Helper+Codable.swift │ └── Models │ ├── Container.swift │ ├── DockerVersion.swift │ ├── Identifier.swift │ ├── Image.swift │ ├── Service.swift │ └── Utilitities.swift └── Tests └── DockerClientTests ├── ContainerTests.swift ├── ImageTests.swift ├── ServiceTests.swift ├── SystemTests.swift └── Utils └── DockerClient+Testable.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | Package.resolved 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Alexander Steiner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "docker-client-swift", 6 | platforms: [.macOS(.v10_15)], 7 | products: [ 8 | .library(name: "DockerClientSwift", targets: ["DockerClientSwift"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.18.0"), 12 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.2.0"), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "DockerClientSwift", 17 | dependencies: [ 18 | .product(name: "NIO", package: "swift-nio"), 19 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 20 | ]), 21 | .testTarget( 22 | name: "DockerClientTests", 23 | dependencies: ["DockerClientSwift"] 24 | ), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Client 2 | [![Language](https://img.shields.io/badge/Swift-5.4-brightgreen.svg)](http://swift.org) 3 | [![Docker Engine API](https://img.shields.io/badge/Docker%20Engine%20API-%20%201.4.1-blue)](https://docs.docker.com/engine/api/v1.41/) 4 | 5 | This is a Docker Client written in Swift. It's using the NIO Framework to communicate with the Docker Engine via sockets. 6 | 7 | 8 | ## Current Use Cases 9 | - [x] List of all images 10 | - [x] List of all containers 11 | - [x] Pull an image 12 | - [x] Create a new container from an image 13 | - [x] Start a container 14 | - [x] Get the stdOut and stdErr output of a container 15 | - [x] Get the docker version information 16 | - [x] Manage the container state 17 | - [x] Create and manage services 18 | - [x] Update services 19 | - [x] List services 20 | - [x] Clean the system (prune containers and images) 21 | 22 | 23 | ## Installation 24 | ### Package.swift 25 | ```Swift 26 | import PackageDescription 27 | 28 | let package = Package( 29 | dependencies: [ 30 | .package(url: "https://github.com/alexsteinerde/docker-client-swift.git", from: "0.1.0"), 31 | ], 32 | targets: [ 33 | .target(name: "App", dependencies: ["DockerClient"]), 34 | ... 35 | ] 36 | ) 37 | ``` 38 | 39 | ### Xcode Project 40 | To add DockerClientSwift to your existing Xcode project, select File -> Swift Packages -> Add Package Depedancy. 41 | Enter `https://github.com/alexsteinerde/docker-client-swift.git` for the URL. 42 | 43 | 44 | ## Usage Example 45 | ```swift 46 | let client = DockerClient() 47 | let image = try client.images.pullImage(byIdentifier: "hello-world:latest").wait() 48 | let container = try client.containers.createContainer(image: image).wait() 49 | try container.start(on: client).wait() 50 | let output = try container.logs(on: client).wait() 51 | try client.syncShutdown() 52 | print(output) 53 | ``` 54 | 55 | For further usage examples, please consider looking at the provided test cases. Or have a look at the demo projects in the next section. 56 | If you want to read more about this package, feel free to read my [blog article](https://alexsteiner.de/blog/posts/docker-client-package-with-swift/) about it. 57 | 58 | ## Demo 59 | There are two demo applications. 60 | 61 | Project | Link 62 | --- | --- 63 | Mac App | [https://github.com/alexsteinerde/docker-client-swift-mac-app](https://github.com/alexsteinerde/docker-client-swift-mac-app) 64 | Vapor App | [https://github.com/alexsteinerde/docker-client-vapor-demo](https://github.com/alexsteinerde/docker-client-vapor-demo) 65 | 66 | 67 | ## Security Advice 68 | When using this in production, make sure you secure your application so no others can execute code. Otherwise, the attacker could access your Docker environment and so all of the containers running in it. 69 | 70 | 71 | ## License 72 | This project is released under the MIT license. See [LICENSE](LICENSE) for details. 73 | 74 | 75 | ## Contribution 76 | 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 :) 77 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/APIs/DockerClient+Container.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | extension DockerClient { 5 | 6 | /// APIs related to containers. 7 | public var containers: ContainersAPI { 8 | .init(client: self) 9 | } 10 | 11 | public struct ContainersAPI { 12 | fileprivate var client: DockerClient 13 | 14 | /// Fetches all containers in the Docker system. 15 | /// - Parameter all: If `true` all containers are fetched, otherwise only running containers. 16 | /// - Throws: Errors that can occur when executing the request. 17 | /// - Returns: Returns an `EventLoopFuture` with a list of `Container`. 18 | public func list(all: Bool=false) throws -> EventLoopFuture<[Container]> { 19 | try client.run(ListContainersEndpoint(all: all)) 20 | .map({ containers in 21 | containers.map { container in 22 | var digest: Digest? 23 | var repositoryTag: Image.RepositoryTag? 24 | if let value = Image.parseNameTagDigest(container.Image) { 25 | (digest, repositoryTag) = value 26 | } else if let repoTag = Image.RepositoryTag(container.Image) { 27 | repositoryTag = repoTag 28 | } 29 | let image = Image(id: .init(container.ImageID), digest: digest, repositoryTags: repositoryTag.map({ [$0]}), createdAt: nil) 30 | return Container(id: .init(container.Id), image: image, createdAt: Date(timeIntervalSince1970: TimeInterval(container.Created)), names: container.Names, state: container.State, command: container.Command) 31 | } 32 | }) 33 | } 34 | 35 | /// Creates a new container from a given image. If specified the commands override the default commands from the image. 36 | /// - Parameters: 37 | /// - image: Instance of an `Image`. 38 | /// - commands: Override the default commands from the image. Default `nil`. 39 | /// - portBindings: Port bindings (forwardings). See ``PortBinding`` for details. Default `[]`. 40 | /// - Throws: Errors that can occur when executing the request. 41 | /// - Returns: Returns an `EventLoopFuture` of a `Container`. 42 | public func createContainer(image: Image, commands: [String]?=nil, portBindings: [PortBinding]=[]) throws -> EventLoopFuture { 43 | let hostConfig: CreateContainerEndpoint.CreateContainerBody.HostConfig? 44 | let exposedPorts: [String: CreateContainerEndpoint.CreateContainerBody.Empty]? 45 | if portBindings.isEmpty { 46 | exposedPorts = nil 47 | hostConfig = nil 48 | } else { 49 | var exposedPortsBuilder: [String: CreateContainerEndpoint.CreateContainerBody.Empty] = [:] 50 | var portBindingsByContainerPort: [String: [CreateContainerEndpoint.CreateContainerBody.HostConfig.PortBinding]] = [:] 51 | for portBinding in portBindings { 52 | let containerPort: String = "\(portBinding.containerPort)/\(portBinding.networkProtocol)" 53 | 54 | exposedPortsBuilder[containerPort] = CreateContainerEndpoint.CreateContainerBody.Empty() 55 | var hostAddresses = portBindingsByContainerPort[containerPort, default: []] 56 | hostAddresses.append( 57 | CreateContainerEndpoint.CreateContainerBody.HostConfig.PortBinding(HostIp: "\(portBinding.hostIP)", HostPort: "\(portBinding.hostPort)")) 58 | portBindingsByContainerPort[containerPort] = hostAddresses 59 | } 60 | exposedPorts = exposedPortsBuilder 61 | hostConfig = CreateContainerEndpoint.CreateContainerBody.HostConfig(PortBindings: portBindingsByContainerPort) 62 | } 63 | return try client.run(CreateContainerEndpoint(imageName: image.id.value, commands: commands, exposedPorts: exposedPorts, hostConfig: hostConfig)) 64 | .flatMap({ response in 65 | try self.get(containerByNameOrId: response.Id) 66 | }) 67 | } 68 | 69 | /// Starts a container. Before starting it needs to be created. 70 | /// - Parameter container: Instance of a created `Container`. 71 | /// - Throws: Errors that can occur when executing the request. 72 | /// - Returns: Returns an `EventLoopFuture` of active actual `PortBinding`s when the container is started. 73 | public func start(container: Container) throws -> EventLoopFuture<[PortBinding]> { 74 | return try client.run(StartContainerEndpoint(containerId: container.id.value)) 75 | .flatMap { _ in 76 | try client.run(InspectContainerEndpoint(nameOrId: container.id.value)) 77 | .flatMapThrowing { response in 78 | try response.NetworkSettings.Ports.flatMap { (containerPortSpec, bindings) in 79 | let containerPortParts = containerPortSpec.split(separator: "/", maxSplits: 2) 80 | guard 81 | let containerPort: UInt16 = UInt16(containerPortParts[0]), 82 | let networkProtocol: NetworkProtocol = NetworkProtocol(rawValue: String(containerPortParts[1])) 83 | else { throw DockerError.message(#"unable to parse port/protocol from NetworkSettings.Ports key - "\#(containerPortSpec)""#) } 84 | 85 | return try (bindings ?? []).compactMap { binding in 86 | guard 87 | let hostPort = UInt16(binding.HostPort) 88 | else { 89 | throw DockerError.message(#"unable to parse port number from NetworkSettings.Ports[].HostPort - "\#(binding.HostPort)""#) 90 | } 91 | 92 | return PortBinding(hostIP: binding.HostIp, hostPort: hostPort, containerPort: containerPort, networkProtocol: networkProtocol) 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | /// Stops a container. Before stopping it needs to be created and started.. 100 | /// - Parameter container: Instance of a started `Container`. 101 | /// - Throws: Errors that can occur when executing the request. 102 | /// - Returns: Returns an `EventLoopFuture` when the container is stopped. 103 | public func stop(container: Container) throws -> EventLoopFuture { 104 | return try client.run(StopContainerEndpoint(containerId: container.id.value)) 105 | .map({ _ in Void() }) 106 | } 107 | 108 | /// Removes an existing container. 109 | /// - Parameter container: Instance of an existing `Container`. 110 | /// - Throws: Errors that can occur when executing the request. 111 | /// - Returns: Returns an `EventLoopFuture` when the container is removed. 112 | public func remove(container: Container) throws -> EventLoopFuture { 113 | return try client.run(RemoveContainerEndpoint(containerId: container.id.value)) 114 | .map({ _ in Void() }) 115 | } 116 | 117 | /// Gets the logs of a container as plain text. This function does not return future log statements but only the once that happen until now. 118 | /// - Parameter container: Instance of a `Container` you want to get the logs for. 119 | /// - Throws: Errors that can occur when executing the request. 120 | /// - Returns: Return an `EventLoopFuture` with the logs as a plain text `String`. 121 | public func logs(container: Container) throws -> EventLoopFuture { 122 | try client.run(GetContainerLogsEndpoint(containerId: container.id.value)) 123 | .map({ response in 124 | // Removing the first character of each line because random characters went there. 125 | response.split(separator: "\n") 126 | .map({ originalLine in 127 | var line = originalLine 128 | line.removeFirst(8) 129 | return String(line) 130 | }) 131 | .joined(separator: "\n") 132 | }) 133 | } 134 | 135 | /// Fetches the latest information about a container by a given name or id.. 136 | /// - Parameter nameOrId: Name or id of a container. 137 | /// - Throws: Errors that can occur when executing the request. 138 | /// - Returns: Returns an `EventLoopFuture` with the `Container` and its information. 139 | public func get(containerByNameOrId nameOrId: String) throws -> EventLoopFuture { 140 | try client.run(InspectContainerEndpoint(nameOrId: nameOrId)) 141 | .map { response in 142 | var digest: Digest? 143 | var repositoryTag: Image.RepositoryTag? 144 | if let value = Image.parseNameTagDigest(response.Image) { 145 | (digest, repositoryTag) = value 146 | } else if let repoTag = Image.RepositoryTag(response.Image) { 147 | repositoryTag = repoTag 148 | } 149 | let image = Image(id: .init(response.Image), digest: digest, repositoryTags: repositoryTag.map({ [$0]}), createdAt: nil) 150 | return Container(id: .init(response.Id), image: image, createdAt: Date.parseDockerDate(response.Created)!, names: [response.Name], state: response.State.Status, command: (response.Config.Cmd ?? []).joined(separator: " ")) 151 | } 152 | } 153 | 154 | 155 | /// Deletes all stopped containers. 156 | /// - Throws: Errors that can occur when executing the request. 157 | /// - Returns: Returns an `EventLoopFuture` with a list of deleted `Container` and the reclaimed space. 158 | public func prune() throws -> EventLoopFuture { 159 | return try client.run(PruneContainersEndpoint()) 160 | .map({ response in 161 | return PrunedContainers(containersIds: response.ContainersDeleted?.map({ .init($0)}) ?? [], reclaimedSpace: response.SpaceReclaimed) 162 | }) 163 | } 164 | 165 | public struct PrunedContainers { 166 | let containersIds: [Identifier] 167 | 168 | /// Disk space reclaimed in bytes 169 | let reclaimedSpace: Int 170 | } 171 | } 172 | } 173 | 174 | extension Container { 175 | /// Starts a container. 176 | /// - Parameter client: A `DockerClient` instance that is used to perform the request. 177 | /// - Throws: Errors that can occur when executing the request. 178 | /// - Returns: Returns an `EventLoopFuture` when the container is started. 179 | public func start(on client: DockerClient) throws -> EventLoopFuture<[PortBinding]> { 180 | try client.containers.start(container: self) 181 | } 182 | 183 | /// Stops a container. 184 | /// - Parameter client: A `DockerClient` instance that is used to perform the request. 185 | /// - Throws: Errors that can occur when executing the request. 186 | /// - Returns: Returns an `EventLoopFuture` when the container is stopped. 187 | public func stop(on client: DockerClient) throws -> EventLoopFuture { 188 | try client.containers.stop(container: self) 189 | } 190 | 191 | /// Removes a container 192 | /// - Parameter client: A `DockerClient` instance that is used to perform the request. 193 | /// - Throws: Errors that can occur when executing the request. 194 | /// - Returns: Returns an `EventLoopFuture` when the container is removed. 195 | public func remove(on client: DockerClient) throws -> EventLoopFuture { 196 | try client.containers.remove(container: self) 197 | } 198 | 199 | /// Gets the logs of a container as plain text. This function does not return future log statements but only the once that happen until now. 200 | /// - Parameter client: A `DockerClient` instance that is used to perform the request. 201 | /// - Throws: Errors that can occur when executing the request. 202 | /// - Returns: Return an `EventLoopFuture` with the logs as a plain text `String`. 203 | public func logs(on client: DockerClient) throws -> EventLoopFuture { 204 | try client.containers.logs(container: self) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/APIs/DockerClient+Image.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | extension DockerClient { 5 | 6 | /// APIs related to images. 7 | public var images: ImagesAPI { 8 | .init(client: self) 9 | } 10 | 11 | public struct ImagesAPI { 12 | fileprivate var client: DockerClient 13 | 14 | /// Pulls an image by it's name. If a tag or digest is specified these are fetched as well. 15 | /// If you want to customize the identifier of the image you can use `pullImage(byIdentifier:)` to do this. 16 | /// - Parameters: 17 | /// - name: Image name that is fetched 18 | /// - tag: Optional tag name. Default is `nil`. 19 | /// - digest: Optional digest value. Default is `nil`. 20 | /// - Throws: Errors that can occur when executing the request. 21 | /// - Returns: Fetches the latest image information and returns an `EventLoopFuture` with the `Image` that has been fetched. 22 | public func pullImage(byName name: String, tag: String?=nil, digest: Digest?=nil) throws -> EventLoopFuture { 23 | var identifier = name 24 | if let tag = tag { 25 | identifier += ":\(tag)" 26 | } 27 | if let digest = digest { 28 | identifier += "@\(digest.rawValue)" 29 | } 30 | return try pullImage(byIdentifier: identifier) 31 | } 32 | 33 | /// Pulls an image by a given identifier. The identifier can be build manually. 34 | /// - Parameter identifier: Identifier of an image that is pulled. 35 | /// - Throws: Errors that can occur when executing the request. 36 | /// - Returns: Fetches the latest image information and returns an `EventLoopFuture` with the `Image` that has been fetched. 37 | public func pullImage(byIdentifier identifier: String) throws -> EventLoopFuture { 38 | return try client.run(PullImageEndpoint(imageName: identifier)) 39 | .flatMap({ _ in 40 | try self.get(imageByNameOrId: identifier) 41 | }) 42 | } 43 | 44 | /// Gets all images in the Docker system. 45 | /// - Parameter all: If `true` intermediate image layer will be returned as well. Default is `false`. 46 | /// - Throws: Errors that can occur when executing the request. 47 | /// - Returns: Returns an `EventLoopFuture` with a list of `Image` instances. 48 | public func list(all: Bool=false) throws -> EventLoopFuture<[Image]> { 49 | try client.run(ListImagesEndpoint(all: all)) 50 | .map({ images in 51 | images.map { image in 52 | Image(id: .init(image.Id), digest: image.RepoDigests?.first.map({ Digest.init($0) }), repoTags: image.RepoTags, createdAt: Date(timeIntervalSince1970: TimeInterval(image.Created))) 53 | } 54 | }) 55 | } 56 | 57 | /// Removes an image. By default only unused images can be removed. If you set `force` to `true` the image will also be removed if it is used. 58 | /// - Parameters: 59 | /// - image: Instance of an `Image` that should be removed. 60 | /// - force: Should the image be removed by force? If `false` the image will only be removed if it's unused. If `true` existing containers will break. Default is `false`. 61 | /// - Throws: Errors that can occur when executing the request. 62 | /// - Returns: Returns an `EventLoopFuture` when the image has been removed or an error is thrown. 63 | public func remove(image: Image, force: Bool=false) throws -> EventLoopFuture { 64 | try client.run(RemoveImageEndpoint(imageId: image.id.value, force: force)) 65 | .map({ _ in Void() }) 66 | } 67 | 68 | /// Fetches the current information about an image from the Docker system. 69 | /// - Parameter nameOrId: Name or id of an image that should be fetched. 70 | /// - Throws: Errors that can occur when executing the request. 71 | /// - Returns: Return an `EventLoopFuture` of the `Image` data. 72 | public func get(imageByNameOrId nameOrId: String) throws -> EventLoopFuture { 73 | try client.run(InspectImagesEndpoint(nameOrId: nameOrId)) 74 | .map { image in 75 | Image(id: .init(image.Id), digest: image.RepoDigests?.first.map({ Digest.init($0) }), repoTags: image.RepoTags, createdAt: Date.parseDockerDate(image.Created)!) 76 | } 77 | } 78 | 79 | 80 | /// Deletes all unused images. 81 | /// - Parameter all: When set to `true`, prune only unused and untagged images. When set to `false`, all unused images are pruned. 82 | /// - Throws: Errors that can occur when executing the request. 83 | /// - Returns: Returns an `EventLoopFuture` with `PrunedImages` details about removed images and the reclaimed space. 84 | public func prune(all: Bool=false) throws -> EventLoopFuture { 85 | return try client.run(PruneImagesEndpoint(dangling: !all)) 86 | .map({ response in 87 | return PrunedImages(imageIds: response.ImagesDeleted?.compactMap(\.Deleted).map({ .init($0)}) ?? [], reclaimedSpace: response.SpaceReclaimed) 88 | }) 89 | } 90 | 91 | public struct PrunedImages { 92 | let imageIds: [Identifier] 93 | 94 | /// Disk space reclaimed in bytes 95 | let reclaimedSpace: Int 96 | } 97 | } 98 | } 99 | 100 | extension Image { 101 | /// Deletes all unused images. 102 | /// - Parameters: 103 | /// - client: A `DockerClient` instance that is used to perform the request. 104 | /// - force: When set to `true`, prune only unused and untagged images. When set to `false`, all unused images are pruned. 105 | /// - Throws: Errors that can occur when executing the request. 106 | /// - Returns: Returns an `EventLoopFuture` with `PrunedImages` details about removed images and the reclaimed space. 107 | public func remove(on client: DockerClient, force: Bool=false) throws -> EventLoopFuture { 108 | try client.images.remove(image: self, force: force) 109 | } 110 | } 111 | 112 | extension Image { 113 | /// Parses an image identifier to it's corresponding digest, name and tag. 114 | /// - Parameter value: Image identifer. 115 | /// - Returns: Returns an `Optional` tuple of a `Digest` and a `RepositoryTag`. 116 | internal static func parseNameTagDigest(_ value: String) -> (Digest, RepositoryTag)? { 117 | let components = value.split(separator: "@").map(String.init) 118 | if components.count == 2, let nameTag = RepositoryTag(components[0]) { 119 | return (.init(components[1]), nameTag) 120 | } else { 121 | return nil 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/APIs/DockerClient+Service.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | extension DockerClient { 5 | 6 | /// APIs related to images. 7 | public var services: ServicesAPI { 8 | .init(client: self) 9 | } 10 | 11 | public struct ServicesAPI { 12 | fileprivate var client: DockerClient 13 | 14 | /// Lists all services running in the Docker instance. 15 | /// - Throws: Errors that can occur when executing the request. 16 | /// - Returns: Returns an `EventLoopFuture` of a list of `Service` instances. 17 | public func list() throws -> EventLoopFuture<[Service]> { 18 | try client.run(ListServicesEndpoint()) 19 | .map({ services in 20 | services.map { service in 21 | service.toService() 22 | } 23 | }) 24 | } 25 | 26 | /// Updates a service with a new image. 27 | /// - Parameters: 28 | /// - service: Instance of a `Service` that should be updated. 29 | /// - newImage: Instance of an `Image` that should be used as the new image for the service. 30 | /// - Throws: Errors that can occur when executing the request. 31 | /// - Returns: Returns an `EventLoopFuture` with the updated `Service`. 32 | public func update(service: Service, newImage: Image) throws -> EventLoopFuture { 33 | try client.run(UpdateServiceEndpoint(nameOrId: service.id.value, name: service.name, version: service.version, image: newImage.id.value)) 34 | .flatMap({ _ in 35 | try self.get(serviceByNameOrId: service.id.value) 36 | }) 37 | } 38 | 39 | /// Gets a service by a given name or id. 40 | /// - Parameter nameOrId: Name or id of a service that should be fetched. 41 | /// - Throws: Errors that can occur when executing the request. 42 | /// - Returns: Return an `EventLoopFuture` with the `Service`. 43 | public func get(serviceByNameOrId nameOrId: String) throws -> EventLoopFuture { 44 | try client.run(InspectServiceEndpoint(nameOrId: nameOrId)) 45 | .map { service in 46 | service.toService() 47 | } 48 | } 49 | 50 | /// Created a new service with a name and an image. 51 | /// This is the minimal way of creating a new service. 52 | /// - Parameters: 53 | /// - name: Name of the new service. 54 | /// - image: Instance of an `Image` for the service. 55 | /// - Throws: Errors that can occur when executing the request. 56 | /// - Returns: Returns an `EventLoopFuture` with the newly created `Service`. 57 | public func create(serviceName name: String, image: Image) throws -> EventLoopFuture { 58 | try client.run(CreateServiceEndpoint(name: name, image: image.id.value)) 59 | .flatMap({ serviceId in 60 | try client.run(InspectServiceEndpoint(nameOrId: serviceId.ID)) 61 | }) 62 | .map({ service in 63 | service.toService() 64 | }) 65 | } 66 | } 67 | } 68 | 69 | extension Service.ServiceResponse { 70 | 71 | /// Internal function that converts the response from Docker to the DockerClient representation. 72 | /// - Returns: Returns an instance of `Service` with the values of the current response. 73 | internal func toService() -> Service { 74 | Service(id: .init(self.ID), name: self.Spec.Name, createdAt: Date.parseDockerDate(self.CreatedAt), updatedAt: Date.parseDockerDate(self.UpdatedAt), version: self.Version.Index, image: Image(id: Identifier(self.Spec.TaskTemplate.ContainerSpec.Image))) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/APIs/DockerClient+System.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | extension DockerClient { 4 | /// Get the version of the Docker runtime. 5 | /// - Throws: Errors that can occur when executing the request. 6 | /// - Returns: Returns an `EventLoopFuture` of the `DockerVersion`. 7 | public func version() throws -> EventLoopFuture { 8 | try run(VersionEndpoint()) 9 | .map({ response in 10 | DockerVersion(version: response.version, architecture: response.arch, kernelVersion: response.kernelVersion, minAPIVersion: response.minAPIVersion, os: response.os) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/APIs/DockerClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | import AsyncHTTPClient 5 | import Logging 6 | 7 | /// The entry point for docker client commands. 8 | public class DockerClient { 9 | private let daemonSocket: String 10 | private let client: HTTPClient 11 | private let logger: Logger 12 | 13 | /// Initialize the `DockerClient`. 14 | /// - Parameters: 15 | /// - daemonSocket: The socket path where the Docker API is listening on. Default is `/var/run/docker.sock`. 16 | /// - client: `HTTPClient` instance that is used to execute the requests. Default is `.init(eventLoopGroupProvider: .createNew)`. 17 | /// - logger: `Logger` for the `DockerClient`. Default is `.init(label: "docker-client")`. 18 | public init(daemonSocket: String = "/var/run/docker.sock", client: HTTPClient = .init(eventLoopGroupProvider: .createNew), logger: Logger = .init(label: "docker-client")) { 19 | self.daemonSocket = daemonSocket 20 | self.client = client 21 | self.logger = logger 22 | } 23 | 24 | /// The client needs to be shutdown otherwise it can crash on exit. 25 | /// - Throws: Throws an error if the `HTTPClient` can not be shutdown. 26 | public func syncShutdown() throws { 27 | try client.syncShutdown() 28 | } 29 | 30 | /// Executes a request to a specific endpoint. The `Endpoint` struct provides all necessary data and parameters for the request. 31 | /// - Parameter endpoint: `Endpoint` instance with all necessary data and parameters. 32 | /// - Throws: It can throw an error when encoding the body of the `Endpoint` request to JSON. 33 | /// - Returns: Returns an `EventLoopFuture` of the expected result definied by the `Endpoint`. 34 | internal func run(_ endpoint: T) throws -> EventLoopFuture { 35 | logger.info("Execute Endpoint: \(endpoint.path)") 36 | 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"), ("Host", "localhost")])) 37 | .logResponseBody(logger) 38 | .decode(as: T.Response.self) 39 | } 40 | 41 | /// Executes a request to a specific endpoint. The `PipelineEndpoint` struct provides all necessary data and parameters for the request. The difference for between `Endpoint` and `EndpointPipeline` is that the second one needs to provide a function that transforms the response as a `String` to the expected result. 42 | /// - Parameter endpoint: `PipelineEndpoint` instance with all necessary data and parameters. 43 | /// - Throws: It can throw an error when encoding the body of the `PipelineEndpoint` request to JSON. 44 | /// - Returns: Returns an `EventLoopFuture` of the expected result definied and transformed by the `PipelineEndpoint`. 45 | internal func run(_ endpoint: T) throws -> EventLoopFuture { 46 | logger.info("Execute PipelineEndpoint: \(endpoint.path)") 47 | 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"), ("Host", "localhost")])) 48 | .logResponseBody(logger) 49 | .mapString(map: endpoint.map(data: )) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/CreateContainerEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct CreateContainerEndpoint: Endpoint { 4 | var body: CreateContainerBody? 5 | 6 | typealias Response = CreateContainerResponse 7 | typealias Body = CreateContainerBody 8 | var method: HTTPMethod = .POST 9 | 10 | private let imageName: String 11 | private let commands: [String]? 12 | 13 | init(imageName: String, commands: [String]?=nil, exposedPorts: [String: CreateContainerBody.Empty]?=nil, hostConfig: CreateContainerBody.HostConfig?=nil) { 14 | self.imageName = imageName 15 | self.commands = commands 16 | self.body = .init(Image: imageName, Cmd: commands, ExposedPorts: exposedPorts, HostConfig: hostConfig) 17 | } 18 | 19 | var path: String { 20 | "containers/create" 21 | } 22 | 23 | struct CreateContainerBody: Codable { 24 | let Image: String 25 | let Cmd: [String]? 26 | let ExposedPorts: [String: Empty]? 27 | let HostConfig: HostConfig? 28 | 29 | struct Empty: Codable {} 30 | 31 | struct HostConfig: Codable { 32 | let PortBindings: [String: [PortBinding]?] 33 | 34 | struct PortBinding: Codable { 35 | let HostIp: String? 36 | let HostPort: String? 37 | } 38 | } 39 | } 40 | 41 | struct CreateContainerResponse: Codable { 42 | let Id: String 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/GetContainerLogsEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct GetContainerLogsEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | 6 | typealias Response = String 7 | var method: HTTPMethod = .GET 8 | 9 | private let containerId: String 10 | 11 | init(containerId: String) { 12 | self.containerId = containerId 13 | } 14 | 15 | var path: String { 16 | "containers/\(containerId)/logs?stdout=true&stderr=true" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/InspectContainerEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct InspectContainerEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | typealias Response = ContainerResponse 6 | var method: HTTPMethod = .GET 7 | 8 | let nameOrId: String 9 | 10 | init(nameOrId: String) { 11 | self.nameOrId = nameOrId 12 | } 13 | 14 | var path: String { 15 | "containers/\(nameOrId)/json" 16 | } 17 | 18 | struct ContainerResponse: Codable { 19 | let Id: String 20 | let Name: String 21 | let Config: ConfigResponse 22 | let Image: String 23 | let Created: String 24 | let State: StateResponse 25 | let NetworkSettings: NetworkSettings 26 | // TODO: Add additional fields 27 | 28 | struct StateResponse: Codable { 29 | var Error: String 30 | var Status: String 31 | } 32 | 33 | struct ConfigResponse: Codable { 34 | let Cmd: [String]? 35 | } 36 | 37 | struct NetworkSettings: Codable { 38 | let Ports: [String: [PortBinding]?] 39 | 40 | struct PortBinding: Codable { 41 | let HostIp: String 42 | let HostPort: String 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/ListContainersEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct ListContainersEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | typealias Response = [ContainerResponse] 6 | var method: HTTPMethod = .GET 7 | 8 | private var all: Bool 9 | 10 | init(all: Bool) { 11 | self.all = all 12 | } 13 | 14 | var path: String { 15 | "containers/json?all=\(all)" 16 | } 17 | 18 | struct ContainerResponse: Codable { 19 | let Id: String 20 | let Names: [String] 21 | let Image: String 22 | let ImageID: String 23 | let Command: String 24 | let Created: Int 25 | let State: String 26 | let Status: String 27 | // TODO: Add additional fields 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/PruneContainersEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct PruneContainersEndpoint: Endpoint { 4 | var body: Body? 5 | 6 | typealias Response = PruneContainersResponse 7 | typealias Body = NoBody 8 | var method: HTTPMethod = .POST 9 | 10 | init() { 11 | 12 | } 13 | 14 | var path: String { 15 | "containers/prune" 16 | } 17 | 18 | struct PruneContainersResponse: Codable { 19 | let ContainersDeleted: [String]? 20 | let SpaceReclaimed: Int 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/RemoveContainerEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct RemoveContainerEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | 6 | typealias Response = NoBody? 7 | var method: HTTPMethod = .DELETE 8 | 9 | private let containerId: String 10 | 11 | init(containerId: String) { 12 | self.containerId = containerId 13 | } 14 | 15 | var path: String { 16 | "containers/\(containerId)" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/StartContainerEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct StartContainerEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | 6 | typealias Response = NoBody? 7 | var method: HTTPMethod = .POST 8 | 9 | private let containerId: String 10 | 11 | init(containerId: String) { 12 | self.containerId = containerId 13 | } 14 | 15 | var path: String { 16 | "containers/\(containerId)/start" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Containers/StopContainerEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct StopContainerEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | 6 | typealias Response = NoBody? 7 | var method: HTTPMethod = .POST 8 | 9 | private let containerId: String 10 | 11 | init(containerId: String) { 12 | self.containerId = containerId 13 | } 14 | 15 | var path: String { 16 | "containers/\(containerId)/stop" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Endpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | import Foundation 3 | 4 | protocol Endpoint { 5 | associatedtype Response: Codable 6 | associatedtype Body: Codable 7 | var path: String { get } 8 | var method: HTTPMethod { get } 9 | var body: Body? { get } 10 | } 11 | 12 | extension Endpoint { 13 | public var body: Body? { 14 | return nil 15 | } 16 | } 17 | 18 | protocol PipelineEndpoint: Endpoint { 19 | func map(data: String) throws -> Self.Response 20 | } 21 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Images/InspectImageEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct InspectImagesEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | typealias Response = ImageResponse 6 | var method: HTTPMethod = .GET 7 | 8 | let nameOrId: String 9 | 10 | init(nameOrId: String) { 11 | self.nameOrId = nameOrId 12 | } 13 | 14 | var path: String { 15 | "images/\(nameOrId)/json" 16 | } 17 | 18 | struct ImageResponse: Codable { 19 | let Id: String 20 | let Parent: String 21 | let Os: String 22 | let Architecture: String 23 | let Created: String 24 | let RepoTags: [String]? 25 | let RepoDigests: [String]? 26 | let Size: Int 27 | let VirtualSize: Int 28 | // TODO: Add additional fields 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Images/ListImagesEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct ListImagesEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | typealias Response = [ImageResponse] 6 | var method: HTTPMethod = .GET 7 | 8 | private var all: Bool 9 | 10 | init(all: Bool) { 11 | self.all = all 12 | } 13 | 14 | var path: String { 15 | "images/json?all=\(all)" 16 | } 17 | 18 | struct ImageResponse: Codable { 19 | let Id: String 20 | let ParentId: String 21 | let RepoTags: [String]? 22 | let RepoDigests: [String]? 23 | let Created: Int 24 | let Size: Int 25 | let VirtualSize: Int 26 | let SharedSize: Int 27 | let Containers: Int 28 | // TODO: Add additional fields 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Images/PruneImagesEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | import Foundation 3 | 4 | struct PruneImagesEndpoint: Endpoint { 5 | var body: Body? 6 | 7 | typealias Response = PruneImagesResponse 8 | typealias Body = NoBody 9 | var method: HTTPMethod = .POST 10 | 11 | private var dangling: Bool 12 | 13 | /// Init 14 | /// - Parameter dangling: When set to `true`, prune only unused *and* untagged images. When set to `false`, all unused images are prune. 15 | init(dangling: Bool=true) { 16 | self.dangling = dangling 17 | } 18 | 19 | var path: String { 20 | "images/prune?filters={\"dangling\": [\"false\"]}" 21 | .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 22 | } 23 | 24 | struct PruneImagesResponse: Codable { 25 | let ImagesDeleted: [PrunedImageResponse]? 26 | let SpaceReclaimed: Int 27 | 28 | struct PrunedImageResponse: Codable { 29 | let Deleted: String? 30 | let Untagged: String? 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Images/PullImageEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct PullImageEndpoint: PipelineEndpoint { 4 | typealias Body = NoBody 5 | 6 | let imageName: String 7 | 8 | var method: HTTPMethod = .POST 9 | var path: String { 10 | "images/create?fromImage=\(imageName)" 11 | } 12 | 13 | typealias Response = PullImageResponse 14 | 15 | struct PullImageResponse: Codable { 16 | let digest: String 17 | } 18 | 19 | func map(data: String) throws -> PullImageResponse { 20 | if let message = try? MessageResponse.decode(from: data) { 21 | throw DockerError.message(message.message) 22 | } 23 | let parts = data.components(separatedBy: "\r\n") 24 | .filter({ $0.count > 0 }) 25 | .compactMap({ try? Status.decode(from: $0) }) 26 | if let digest = parts.last(where: { $0.status.hasPrefix("Digest:")}) 27 | .map({ (status) -> String in 28 | status.status.replacingOccurrences(of: "Digest: ", with: "") 29 | }) { 30 | return .init(digest: digest) 31 | } else { 32 | throw DockerError.unknownResponse(data) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Images/RemoveImageEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct RemoveImageEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | 6 | typealias Response = NoBody? 7 | var method: HTTPMethod = .DELETE 8 | 9 | private let imageId: String 10 | private let force: Bool 11 | 12 | init(imageId: String, force: Bool=false) { 13 | self.imageId = imageId 14 | self.force = force 15 | } 16 | 17 | var path: String { 18 | "images/\(imageId)?force=\(force ? "true" : "false")" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Services/CreateServiceEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct CreateServiceEndpoint: Endpoint { 4 | var body: Body? 5 | 6 | typealias Response = CreateServiceResponse 7 | typealias Body = Service.UpdateServiceBody 8 | var method: HTTPMethod = .POST 9 | 10 | private let name: String 11 | private let image: String 12 | 13 | init(name: String, image: String) { 14 | self.name = name 15 | self.image = image 16 | self.body = Service.UpdateServiceBody(Name: name, TaskTemplate: .init(ContainerSpec: .init(Image: image))) 17 | } 18 | 19 | var path: String { 20 | "services/create" 21 | } 22 | 23 | struct CreateServiceResponse: Codable { 24 | let ID: String 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Services/InspectServiceEndpoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | struct InspectServiceEndpoint: Endpoint { 5 | typealias Body = NoBody 6 | typealias Response = Service.ServiceResponse 7 | var method: HTTPMethod = .GET 8 | 9 | 10 | private let nameOrId: String 11 | init(nameOrId: String) { 12 | self.nameOrId = nameOrId 13 | } 14 | var path: String { 15 | "services/\(nameOrId)" 16 | } 17 | } 18 | 19 | internal extension Service { 20 | struct ServiceResponse: Codable { 21 | let ID: String 22 | let Version: ServiceVersionResponse 23 | let CreatedAt: String 24 | let UpdatedAt: String 25 | let Spec: ServiceSpecResponse 26 | 27 | struct ServiceVersionResponse: Codable { 28 | let Index: Int 29 | } 30 | 31 | struct ServiceSpecResponse: Codable { 32 | let Name: String 33 | let TaskTemplate: TaskTemplateResponse 34 | } 35 | 36 | struct TaskTemplateResponse: Codable { 37 | let ContainerSpec: ContainerSpecResponse 38 | } 39 | 40 | struct ContainerSpecResponse: Codable { 41 | let Image: String 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Services/ListServicesEndpoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | struct ListServicesEndpoint: Endpoint { 5 | typealias Body = NoBody 6 | typealias Response = [Service.ServiceResponse] 7 | var method: HTTPMethod = .GET 8 | 9 | init() { 10 | } 11 | 12 | var path: String { 13 | "services" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/Services/UpdateServiceEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct UpdateServiceEndpoint: Endpoint { 4 | var body: Body? 5 | 6 | typealias Response = NoBody? 7 | typealias Body = Service.UpdateServiceBody 8 | var method: HTTPMethod = .POST 9 | 10 | private let nameOrId: String 11 | private let version: Int 12 | private let image: String? 13 | private let name: String 14 | 15 | init(nameOrId: String, name: String, version: Int, image: String?) { 16 | self.nameOrId = nameOrId 17 | self.name = name 18 | self.version = version 19 | self.image = image 20 | 21 | self.body = .init(Name: name, TaskTemplate: .init(ContainerSpec: .init(Image: image))) 22 | } 23 | 24 | var path: String { 25 | "services/\(nameOrId)/update?version=\(version)" 26 | } 27 | } 28 | 29 | extension Service { 30 | struct UpdateServiceBody: Codable { 31 | let Name: String 32 | let TaskTemplate: TaskTemplateBody 33 | struct TaskTemplateBody: Codable { 34 | let ContainerSpec: ContainerSpecBody 35 | } 36 | 37 | struct ContainerSpecBody: Codable { 38 | let Image: String? 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/System/SystemInformationEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct SystemInformationEndpoint: Endpoint { 4 | typealias Body = NoBody 5 | 6 | var method: HTTPMethod = .GET 7 | let path: String = "info" 8 | 9 | typealias Response = SystemInformationResponse 10 | 11 | // MARK: - SystemInformationResponse 12 | struct SystemInformationResponse: Codable { 13 | let id: String 14 | let containers, containersRunning, containersPaused, containersStopped: Int 15 | let images: Int 16 | let driver: String 17 | let driverStatus: [[String]] 18 | let dockerRootDir: String 19 | let plugins: Plugins? 20 | let memoryLimit, swapLimit, kernelMemory, cpuCfsPeriod: Bool 21 | let cpuCfsQuota, cpuShares, cpuSet, pidsLimit: Bool 22 | let oomKillDisable, iPv4Forwarding, bridgeNfIptables, bridgeNfIp6Tables: Bool 23 | let debug: Bool 24 | let nFd, nGoroutines: Int 25 | let systemTime, loggingDriver, cgroupDriver, cgroupVersion: String 26 | let nEventsListener: Int 27 | let kernelVersion, operatingSystem, osVersion, osType: String 28 | let architecture: String 29 | let ncpu, memTotal: Int 30 | let indexServerAddress: String 31 | let registryConfig: RegistryConfig 32 | let genericResources: [GenericResource]? 33 | let httpProxy: String 34 | let httpsProxy: String 35 | let noProxy, name: String 36 | let labels: [String] 37 | let experimentalBuild: Bool 38 | let serverVersion, clusterStore, clusterAdvertise: String? 39 | let runtimes: Runtimes? 40 | let defaultRuntime: String 41 | let swarm: Swarm? 42 | let liveRestoreEnabled: Bool 43 | let isolation, initBinary: String 44 | let containerdCommit, runcCommit, initCommit: Commit 45 | let securityOptions: [String] 46 | let productLicense: String 47 | let defaultAddressPools: [DefaultAddressPool]? 48 | let warnings: [String]? 49 | 50 | enum CodingKeys: String, CodingKey { 51 | case id = "ID" 52 | case containers = "Containers" 53 | case containersRunning = "ContainersRunning" 54 | case containersPaused = "ContainersPaused" 55 | case containersStopped = "ContainersStopped" 56 | case images = "Images" 57 | case driver = "Driver" 58 | case driverStatus = "DriverStatus" 59 | case dockerRootDir = "DockerRootDir" 60 | case plugins = "Plugins" 61 | case memoryLimit = "MemoryLimit" 62 | case swapLimit = "SwapLimit" 63 | case kernelMemory = "KernelMemory" 64 | case cpuCfsPeriod = "CpuCfsPeriod" 65 | case cpuCfsQuota = "CpuCfsQuota" 66 | case cpuShares = "CPUShares" 67 | case cpuSet = "CPUSet" 68 | case pidsLimit = "PidsLimit" 69 | case oomKillDisable = "OomKillDisable" 70 | case iPv4Forwarding = "IPv4Forwarding" 71 | case bridgeNfIptables = "BridgeNfIptables" 72 | case bridgeNfIp6Tables = "BridgeNfIp6tables" 73 | case debug = "Debug" 74 | case nFd = "NFd" 75 | case nGoroutines = "NGoroutines" 76 | case systemTime = "SystemTime" 77 | case loggingDriver = "LoggingDriver" 78 | case cgroupDriver = "CgroupDriver" 79 | case cgroupVersion = "CgroupVersion" 80 | case nEventsListener = "NEventsListener" 81 | case kernelVersion = "KernelVersion" 82 | case operatingSystem = "OperatingSystem" 83 | case osVersion = "OSVersion" 84 | case osType = "OSType" 85 | case architecture = "Architecture" 86 | case ncpu = "NCPU" 87 | case memTotal = "MemTotal" 88 | case indexServerAddress = "IndexServerAddress" 89 | case registryConfig = "RegistryConfig" 90 | case genericResources = "GenericResources" 91 | case httpProxy = "HttpProxy" 92 | case httpsProxy = "HttpsProxy" 93 | case noProxy = "NoProxy" 94 | case name = "Name" 95 | case labels = "Labels" 96 | case experimentalBuild = "ExperimentalBuild" 97 | case serverVersion = "ServerVersion" 98 | case clusterStore = "ClusterStore" 99 | case clusterAdvertise = "ClusterAdvertise" 100 | case runtimes = "Runtimes" 101 | case defaultRuntime = "DefaultRuntime" 102 | case swarm = "Swarm" 103 | case liveRestoreEnabled = "LiveRestoreEnabled" 104 | case isolation = "Isolation" 105 | case initBinary = "InitBinary" 106 | case containerdCommit = "ContainerdCommit" 107 | case runcCommit = "RuncCommit" 108 | case initCommit = "InitCommit" 109 | case securityOptions = "SecurityOptions" 110 | case productLicense = "ProductLicense" 111 | case defaultAddressPools = "DefaultAddressPools" 112 | case warnings = "Warnings" 113 | } 114 | } 115 | 116 | // MARK: - Commit 117 | struct Commit: Codable { 118 | let id, expected: String 119 | 120 | enum CodingKeys: String, CodingKey { 121 | case id = "ID" 122 | case expected = "Expected" 123 | } 124 | } 125 | 126 | // MARK: - DefaultAddressPool 127 | struct DefaultAddressPool: Codable { 128 | let base, size: String 129 | 130 | enum CodingKeys: String, CodingKey { 131 | case base = "Base" 132 | case size = "Size" 133 | } 134 | } 135 | 136 | // MARK: - GenericResource 137 | struct GenericResource: Codable { 138 | let discreteResourceSpec: DiscreteResourceSpec? 139 | let namedResourceSpec: NamedResourceSpec? 140 | 141 | enum CodingKeys: String, CodingKey { 142 | case discreteResourceSpec = "DiscreteResourceSpec" 143 | case namedResourceSpec = "NamedResourceSpec" 144 | } 145 | } 146 | 147 | // MARK: - DiscreteResourceSpec 148 | struct DiscreteResourceSpec: Codable { 149 | let kind: String 150 | let value: Int 151 | 152 | enum CodingKeys: String, CodingKey { 153 | case kind = "Kind" 154 | case value = "Value" 155 | } 156 | } 157 | 158 | // MARK: - NamedResourceSpec 159 | struct NamedResourceSpec: Codable { 160 | let kind, value: String 161 | 162 | enum CodingKeys: String, CodingKey { 163 | case kind = "Kind" 164 | case value = "Value" 165 | } 166 | } 167 | 168 | // MARK: - Plugins 169 | struct Plugins: Codable { 170 | let volume, network, authorization, log: [String]? 171 | 172 | enum CodingKeys: String, CodingKey { 173 | case volume = "Volume" 174 | case network = "Network" 175 | case authorization = "Authorization" 176 | case log = "Log" 177 | } 178 | } 179 | 180 | // MARK: - RegistryConfig 181 | struct RegistryConfig: Codable { 182 | let allowNondistributableArtifactsCIDRs, allowNondistributableArtifactsHostnames, insecureRegistryCIDRs: [String] 183 | let indexConfigs: [String: IndexConfig] 184 | let mirrors: [String] 185 | 186 | enum CodingKeys: String, CodingKey { 187 | case allowNondistributableArtifactsCIDRs = "AllowNondistributableArtifactsCIDRs" 188 | case allowNondistributableArtifactsHostnames = "AllowNondistributableArtifactsHostnames" 189 | case insecureRegistryCIDRs = "InsecureRegistryCIDRs" 190 | case indexConfigs = "IndexConfigs" 191 | case mirrors = "Mirrors" 192 | } 193 | } 194 | 195 | // MARK: - IndexConfig 196 | struct IndexConfig: Codable { 197 | let name: String 198 | let mirrors: [String] 199 | let secure, official: Bool 200 | 201 | enum CodingKeys: String, CodingKey { 202 | case name = "Name" 203 | case mirrors = "Mirrors" 204 | case secure = "Secure" 205 | case official = "Official" 206 | } 207 | } 208 | 209 | // MARK: - Runtimes 210 | struct Runtimes: Codable { 211 | let runc, runcMaster: Runc? 212 | let custom: Custom? 213 | 214 | enum CodingKeys: String, CodingKey { 215 | case runc 216 | case runcMaster = "runc-master" 217 | case custom 218 | } 219 | } 220 | 221 | // MARK: - Custom 222 | struct Custom: Codable { 223 | let path: String 224 | let runtimeArgs: [String] 225 | } 226 | 227 | // MARK: - Runc 228 | struct Runc: Codable { 229 | let path: String 230 | } 231 | 232 | // MARK: - Swarm 233 | struct Swarm: Codable { 234 | let nodeID, nodeAddr, localNodeState: String 235 | let controlAvailable: Bool 236 | let error: String 237 | let remoteManagers: [RemoteManager]? 238 | let nodes, managers: Int? 239 | let cluster: Cluster? 240 | 241 | enum CodingKeys: String, CodingKey { 242 | case nodeID = "NodeID" 243 | case nodeAddr = "NodeAddr" 244 | case localNodeState = "LocalNodeState" 245 | case controlAvailable = "ControlAvailable" 246 | case error = "Error" 247 | case remoteManagers = "RemoteManagers" 248 | case nodes = "Nodes" 249 | case managers = "Managers" 250 | case cluster = "Cluster" 251 | } 252 | } 253 | 254 | // MARK: - Cluster 255 | struct Cluster: Codable { 256 | let id: String 257 | let version: Version 258 | let createdAt, updatedAt: String 259 | let spec: Spec 260 | let tlsInfo: TLSInfo 261 | let rootRotationInProgress: Bool 262 | let dataPathPort: Int 263 | let defaultAddrPool: [[String]]? 264 | let subnetSize: Int 265 | 266 | enum CodingKeys: String, CodingKey { 267 | case id = "ID" 268 | case version = "Version" 269 | case createdAt = "CreatedAt" 270 | case updatedAt = "UpdatedAt" 271 | case spec = "Spec" 272 | case tlsInfo = "TLSInfo" 273 | case rootRotationInProgress = "RootRotationInProgress" 274 | case dataPathPort = "DataPathPort" 275 | case defaultAddrPool = "DefaultAddrPool" 276 | case subnetSize = "SubnetSize" 277 | } 278 | } 279 | 280 | // MARK: - Spec 281 | struct Spec: Codable { 282 | let name: String 283 | let labels: Labels 284 | let orchestration: Orchestration 285 | let raft: Raft 286 | let dispatcher: Dispatcher 287 | let caConfig: CAConfig 288 | let encryptionConfig: EncryptionConfig 289 | let taskDefaults: TaskDefaults 290 | 291 | enum CodingKeys: String, CodingKey { 292 | case name = "Name" 293 | case labels = "Labels" 294 | case orchestration = "Orchestration" 295 | case raft = "Raft" 296 | case dispatcher = "Dispatcher" 297 | case caConfig = "CAConfig" 298 | case encryptionConfig = "EncryptionConfig" 299 | case taskDefaults = "TaskDefaults" 300 | } 301 | } 302 | 303 | // MARK: - CAConfig 304 | struct CAConfig: Codable { 305 | let nodeCERTExpiry: Int 306 | let externalCAs: [ExternalCA] 307 | let signingCACERT, signingCAKey: String 308 | let forceRotate: Int 309 | 310 | enum CodingKeys: String, CodingKey { 311 | case nodeCERTExpiry = "NodeCertExpiry" 312 | case externalCAs = "ExternalCAs" 313 | case signingCACERT = "SigningCACert" 314 | case signingCAKey = "SigningCAKey" 315 | case forceRotate = "ForceRotate" 316 | } 317 | } 318 | 319 | // MARK: - ExternalCA 320 | struct ExternalCA: Codable { 321 | let externalCAProtocol, url: String 322 | let options: ExternalCAOptions 323 | let caCERT: String 324 | 325 | enum CodingKeys: String, CodingKey { 326 | case externalCAProtocol = "Protocol" 327 | case url = "URL" 328 | case options = "Options" 329 | case caCERT = "CACert" 330 | } 331 | } 332 | 333 | // MARK: - ExternalCAOptions 334 | struct ExternalCAOptions: Codable { 335 | let property1, property2: String 336 | } 337 | 338 | // MARK: - Dispatcher 339 | struct Dispatcher: Codable { 340 | let heartbeatPeriod: Int 341 | 342 | enum CodingKeys: String, CodingKey { 343 | case heartbeatPeriod = "HeartbeatPeriod" 344 | } 345 | } 346 | 347 | // MARK: - EncryptionConfig 348 | struct EncryptionConfig: Codable { 349 | let autoLockManagers: Bool 350 | 351 | enum CodingKeys: String, CodingKey { 352 | case autoLockManagers = "AutoLockManagers" 353 | } 354 | } 355 | 356 | // MARK: - Labels 357 | struct Labels: Codable { 358 | let comExampleCorpType, comExampleCorpDepartment: String 359 | 360 | enum CodingKeys: String, CodingKey { 361 | case comExampleCorpType = "com.example.corp.type" 362 | case comExampleCorpDepartment = "com.example.corp.department" 363 | } 364 | } 365 | 366 | // MARK: - Orchestration 367 | struct Orchestration: Codable { 368 | let taskHistoryRetentionLimit: Int 369 | 370 | enum CodingKeys: String, CodingKey { 371 | case taskHistoryRetentionLimit = "TaskHistoryRetentionLimit" 372 | } 373 | } 374 | 375 | // MARK: - Raft 376 | struct Raft: Codable { 377 | let snapshotInterval, keepOldSnapshots, logEntriesForSlowFollowers, electionTick: Int 378 | let heartbeatTick: Int 379 | 380 | enum CodingKeys: String, CodingKey { 381 | case snapshotInterval = "SnapshotInterval" 382 | case keepOldSnapshots = "KeepOldSnapshots" 383 | case logEntriesForSlowFollowers = "LogEntriesForSlowFollowers" 384 | case electionTick = "ElectionTick" 385 | case heartbeatTick = "HeartbeatTick" 386 | } 387 | } 388 | 389 | // MARK: - TaskDefaults 390 | struct TaskDefaults: Codable { 391 | let logDriver: LogDriver 392 | 393 | enum CodingKeys: String, CodingKey { 394 | case logDriver = "LogDriver" 395 | } 396 | } 397 | 398 | // MARK: - LogDriver 399 | struct LogDriver: Codable { 400 | let name: String 401 | let options: LogDriverOptions 402 | 403 | enum CodingKeys: String, CodingKey { 404 | case name = "Name" 405 | case options = "Options" 406 | } 407 | } 408 | 409 | // MARK: - LogDriverOptions 410 | struct LogDriverOptions: Codable { 411 | let maxFile, maxSize: String 412 | 413 | enum CodingKeys: String, CodingKey { 414 | case maxFile = "max-file" 415 | case maxSize = "max-size" 416 | } 417 | } 418 | 419 | // MARK: - TLSInfo 420 | struct TLSInfo: Codable { 421 | let trustRoot, certIssuerSubject, certIssuerPublicKey: String 422 | 423 | enum CodingKeys: String, CodingKey { 424 | case trustRoot = "TrustRoot" 425 | case certIssuerSubject = "CertIssuerSubject" 426 | case certIssuerPublicKey = "CertIssuerPublicKey" 427 | } 428 | } 429 | 430 | // MARK: - Version 431 | struct Version: Codable { 432 | let index: Int 433 | 434 | enum CodingKeys: String, CodingKey { 435 | case index = "Index" 436 | } 437 | } 438 | 439 | // MARK: - RemoteManager 440 | struct RemoteManager: Codable { 441 | let nodeID, addr: String 442 | 443 | enum CodingKeys: String, CodingKey { 444 | case nodeID = "NodeID" 445 | case addr = "Addr" 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Endpoints/System/VersionEndpoint.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | struct NoBody: Codable {} 4 | 5 | struct VersionEndpoint: Endpoint { 6 | typealias Body = NoBody 7 | 8 | var method: HTTPMethod = .GET 9 | let path: String = "version" 10 | 11 | typealias Response = VersionResponse 12 | 13 | struct VersionResponse: Codable { 14 | let platform: Platform 15 | let components: [Component] 16 | let version, apiVersion, minAPIVersion, gitCommit: String 17 | let goVersion, os, arch, kernelVersion: String 18 | let buildTime: String 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case platform = "Platform" 22 | case components = "Components" 23 | case version = "Version" 24 | case apiVersion = "ApiVersion" 25 | case minAPIVersion = "MinAPIVersion" 26 | case gitCommit = "GitCommit" 27 | case goVersion = "GoVersion" 28 | case os = "Os" 29 | case arch = "Arch" 30 | case kernelVersion = "KernelVersion" 31 | case buildTime = "BuildTime" 32 | } 33 | } 34 | 35 | // MARK: - Component 36 | struct Component: Codable { 37 | let name, version: String 38 | let details: Details 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case name = "Name" 42 | case version = "Version" 43 | case details = "Details" 44 | } 45 | } 46 | 47 | // MARK: - Details 48 | struct Details: Codable { 49 | let apiVersion, arch, buildTime, experimental: String? 50 | let gitCommit: String 51 | let goVersion, kernelVersion, minAPIVersion, os: String? 52 | 53 | enum CodingKeys: String, CodingKey { 54 | case apiVersion = "ApiVersion" 55 | case arch = "Arch" 56 | case buildTime = "BuildTime" 57 | case experimental = "Experimental" 58 | case gitCommit = "GitCommit" 59 | case goVersion = "GoVersion" 60 | case kernelVersion = "KernelVersion" 61 | case minAPIVersion = "MinAPIVersion" 62 | case os = "Os" 63 | } 64 | } 65 | 66 | // MARK: - Platform 67 | struct Platform: Codable { 68 | let name: String 69 | 70 | enum CodingKeys: String, CodingKey { 71 | case name = "Name" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Helper/DateParsing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | 5 | /// Some API results contain dates in a specific date format. It isn't ISO-8601 but a different one. 6 | /// - Parameter string: String of the date to parse. 7 | /// - Returns: Returns a `Date` instance if the string is in the correct format. Otherwise nil is returned. 8 | static func parseDockerDate(_ string: String) -> Date? { 9 | let format = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSS'Z'" 10 | let formatter = DateFormatter() 11 | formatter.dateFormat = format 12 | return formatter.date(from: string) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Helper/EventLoopFuture+FlatMap.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | extension EventLoopFuture { 4 | /// `flatMap` functions that accepts a throwing callback. 5 | /// - Parameters: 6 | /// - callback: Throwing closure that returns and `EventLoopFuture` of the result type. 7 | /// - Returns: Returns an `EventLoopFuture` with the value of the callback future. 8 | @inlinable public func flatMap(file: StaticString = #file, line: UInt = #line, _ callback: @escaping (Value) throws -> EventLoopFuture) -> EventLoopFuture { 9 | self.flatMap { value in 10 | do { 11 | return try callback(value) 12 | } catch { 13 | return self.eventLoop.makeFailedFuture(error) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Helper/HTTPClient+Codable.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2018-2019 Swift Server Working Group and the AsyncHTTPClient project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import NIO 17 | import AsyncHTTPClient 18 | import Logging 19 | 20 | extension EventLoopFuture where Value == HTTPClient.Response { 21 | /// Logs the response body to the specififed logger. 22 | /// - Parameter logger: Logger the message should be logged to. 23 | /// - Returns: Returnes the original response of the `HTTPClient`. 24 | func logResponseBody(_ logger: Logger) -> EventLoopFuture { 25 | self.always({ ( result: Result) in 26 | logger.debug("Response: \(result.bodyValue() ?? "No Body Data")") 27 | }) 28 | } 29 | } 30 | 31 | extension Result where Success == HTTPClient.Response { 32 | func bodyValue() -> String? { 33 | (try? self.get()).flatMap({ response -> String? in 34 | if let bodyData = response.bodyData { 35 | return String(data: bodyData, encoding: .utf8) 36 | } else { 37 | return nil 38 | } 39 | }) 40 | } 41 | } 42 | 43 | extension EventLoopFuture where Value == HTTPClient.Response { 44 | 45 | public enum BodyError : Swift.Error { 46 | case noBodyData 47 | } 48 | 49 | /// Decode the response body as T using the given decoder. 50 | /// 51 | /// - parameters: 52 | /// - type: The type to decode. Must conform to Decoable. 53 | /// - decoder: The decoder used to decode the reponse body. Defaults to JSONDecoder. 54 | /// - returns: A future decoded type. 55 | /// - throws: BodyError.noBodyData when no body is found in reponse. 56 | public func decode(as type: T.Type, decoder: Decoder = JSONDecoder()) -> EventLoopFuture { 57 | flatMapThrowing { response -> T in 58 | try response.checkStatusCode() 59 | if T.self == NoBody.self || T.self == NoBody?.self { 60 | return NoBody() as! T 61 | } 62 | 63 | guard let bodyData = response.bodyData else { 64 | throw BodyError.noBodyData 65 | } 66 | if T.self == String.self { 67 | return String(data: bodyData, encoding: .utf8) as! T 68 | } 69 | return try decoder.decode(type, from: bodyData) 70 | } 71 | } 72 | 73 | public func mapString(map: @escaping (String) throws -> T) -> EventLoopFuture { 74 | flatMapThrowing { (response) -> T in 75 | try response.checkStatusCode() 76 | guard let bodyData = response.bodyData else { 77 | throw BodyError.noBodyData 78 | } 79 | guard let string = String(data: bodyData, encoding: .utf8) else { 80 | throw BodyError.noBodyData 81 | } 82 | return try map(string) 83 | } 84 | } 85 | 86 | /// Decode the response body as T using the given decoder. 87 | /// 88 | /// - parameters: 89 | /// - type: The type to decode. Must conform to Decoable. 90 | /// - decoder: The decoder used to decode the reponse body. Defaults to JSONDecoder. 91 | /// - returns: A future optional decoded type. The future value will be nil when no body is present in the response. 92 | public func decode(as type: T.Type, decoder: Decoder = JSONDecoder()) -> EventLoopFuture { 93 | flatMapThrowing { response -> T? in 94 | try response.checkStatusCode() 95 | guard let bodyData = response.bodyData else { 96 | return nil 97 | } 98 | 99 | return try decoder.decode(type, from: bodyData) 100 | } 101 | } 102 | } 103 | 104 | extension HTTPClient.Response { 105 | 106 | /// This function checks the current response fot the status code. If it is not in the range of `200...299` it throws an error 107 | /// - Throws: Throws a `DockerError.errorCode` error. If the response is a `MessageResponse` it uses the `message` content for the message, otherwise the body will be used. 108 | fileprivate func checkStatusCode() throws { 109 | guard 200...299 ~= self.status.code else { 110 | if let data = self.bodyData, let message = try? MessageResponse.decode(from: data) { 111 | throw DockerError.errorCode(Int(self.status.code), message.message) 112 | } else { 113 | throw DockerError.errorCode(Int(self.status.code), self.bodyData.map({ String(data: $0, encoding: .utf8) ?? "" })) 114 | } 115 | } 116 | } 117 | 118 | public var bodyData : Data? { 119 | guard let bodyBuffer = body, 120 | let bodyBytes = bodyBuffer.getBytes(at: bodyBuffer.readerIndex, length: bodyBuffer.readableBytes) else { 121 | return nil 122 | } 123 | 124 | return Data(bodyBytes) 125 | } 126 | 127 | } 128 | 129 | public protocol Decoder { 130 | func decode(_ type: T.Type, from: Data) throws -> T where T : Decodable 131 | } 132 | 133 | extension JSONDecoder : Decoder {} 134 | extension PropertyListDecoder : Decoder {} 135 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Helper/HTTPClient+ExecuteOnSocket.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | import AsyncHTTPClient 5 | import Logging 6 | 7 | extension HTTPClient { 8 | /// Executes a HTTP request on a socket. 9 | /// - Parameters: 10 | /// - method: HTTP method. 11 | /// - socketPath: The path to the unix domain socket to connect to. 12 | /// - urlPath: The URI path and query that will be sent to the server. 13 | /// - body: Request body. 14 | /// - deadline: Point in time by which the request must complete. 15 | /// - logger: The logger to use for this request. 16 | /// - headers: Custom HTTP headers. 17 | /// - Returns: Returns an `EventLoopFuture` with the `Response` of the request 18 | public func execute(_ method: HTTPMethod = .GET, socketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger, headers: HTTPHeaders) -> EventLoopFuture { 19 | do { 20 | guard let url = URL(httpURLWithSocketPath: socketPath, uri: urlPath) else { 21 | throw HTTPClientError.invalidURL 22 | } 23 | let request = try Request(url: url, method: method, headers: headers, body: body) 24 | return self.execute(request: request, deadline: deadline, logger: logger) 25 | } catch { 26 | return self.eventLoopGroup.next().makeFailedFuture(error) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Helper/Helper+Codable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Encodable { 4 | func encode(with encoder: JSONEncoder = JSONEncoder()) throws -> Data { 5 | return try encoder.encode(self) 6 | } 7 | } 8 | 9 | extension Decodable { 10 | static func decode(with decoder: JSONDecoder = JSONDecoder(), from data: Data) throws -> Self { 11 | return try decoder.decode(Self.self, from: data) 12 | } 13 | 14 | static func decode(with decoder: JSONDecoder = JSONDecoder(), from string: String) throws -> Self { 15 | return try decoder.decode(Self.self, from: string.data(using: .utf8)!) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Models/Container.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | /// Representation of a container. 3 | /// Some actions can be performed on an instance. 4 | public struct Container { 5 | public var id: Identifier 6 | public var image: Image 7 | public var createdAt: Date 8 | public var names: [String] 9 | public var state: String 10 | public var command: String 11 | } 12 | 13 | extension Container: Codable {} 14 | 15 | /// Representation of a port binding 16 | public struct PortBinding { 17 | public var hostIP: String 18 | public var hostPort: UInt16 19 | public var containerPort: UInt16 20 | public var networkProtocol: NetworkProtocol 21 | 22 | /// Creates a PortBinding 23 | /// 24 | /// - Parameters: 25 | /// - hostIP: The host IP address to map the connection to. Default `0.0.0.0`. 26 | /// - hostPort: The port on the Docker host to map connections to. `0` means map to a random available port. Default `0`. 27 | /// - containerPort: The port on the container to map connections from. 28 | /// - networkProtocol: The protocol (`tcp`/`udp`) to bind. Default `tcp`. 29 | public init(hostIP: String = "0.0.0.0", hostPort: UInt16=0, containerPort: UInt16, networkProtocol: NetworkProtocol = .tcp) { 30 | self.hostIP = hostIP 31 | self.hostPort = hostPort 32 | self.containerPort = containerPort 33 | self.networkProtocol = networkProtocol 34 | } 35 | } 36 | 37 | public enum NetworkProtocol: String { 38 | case tcp 39 | case udp 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Models/DockerVersion.swift: -------------------------------------------------------------------------------- 1 | public struct DockerVersion { 2 | public let version: String 3 | public let architecture: String 4 | public let kernelVersion: String 5 | public let minAPIVersion: String 6 | public let os: String 7 | } 8 | 9 | extension DockerVersion: Codable {} 10 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Models/Identifier.swift: -------------------------------------------------------------------------------- 1 | public struct Identifier { 2 | public init(_ value: String) { 3 | self.value = value 4 | } 5 | 6 | public typealias StringLiteralType = String 7 | 8 | public var value: String 9 | } 10 | 11 | extension Identifier: ExpressibleByStringLiteral { 12 | public init(stringLiteral value: String) { 13 | self.value = value 14 | } 15 | } 16 | 17 | extension Identifier: Equatable { } 18 | 19 | extension Identifier: Codable {} 20 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Models/Image.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Representation of an image. 4 | /// Some actions can be performed on an instance. 5 | public struct Image { 6 | 7 | /// Local ID of the image. This can vary from instant to instant. 8 | public var id: Identifier 9 | 10 | /// The unique hash of the image layer that is exposed. Format: {hash algorithm}:{hash value}. 11 | public var digest: Digest? 12 | 13 | /// Names and tags of the image that it has in the repository. 14 | public var repositoryTags: [RepositoryTag] 15 | 16 | /// Date when the image was created. 17 | public var createdAt: Date? 18 | 19 | public struct RepositoryTag { 20 | public var repository: String 21 | public var tag: String? 22 | } 23 | 24 | internal init(id: Identifier, digest: Digest? = nil, repoTags: [String]?=nil, createdAt: Date?=nil) { 25 | let repositoryTags = repoTags.map({ repoTags in 26 | repoTags.compactMap { repoTag in 27 | return RepositoryTag(repoTag) 28 | } 29 | }) ?? [] 30 | 31 | self.init(id: id, digest: digest, repositoryTags: repositoryTags, createdAt: createdAt) 32 | } 33 | 34 | internal init(id: Identifier, digest: Digest? = nil, repositoryTags: [RepositoryTag]?=nil, createdAt: Date?=nil) { 35 | self.id = id 36 | self.digest = digest 37 | self.createdAt = createdAt 38 | self.repositoryTags = repositoryTags ?? [] 39 | } 40 | 41 | internal init(id: Identifier) { 42 | self.id = id 43 | self.repositoryTags = [] 44 | } 45 | } 46 | 47 | extension Image.RepositoryTag { 48 | init?(_ value: String) { 49 | guard !value.hasPrefix("sha256") else { return nil } 50 | let components = value.split(separator: ":").map(String.init) 51 | if components.count == 2 { 52 | self.repository = components[0] 53 | self.tag = components[1] 54 | } else if components.count == 1 { 55 | self.repository = value 56 | } else { 57 | return nil 58 | } 59 | } 60 | } 61 | 62 | extension Image: Codable {} 63 | extension Image.RepositoryTag: Codable {} 64 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Models/Service.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Representation of a service. 4 | /// Some actions can be performed on an instance. 5 | public struct Service { 6 | public let id: Identifier 7 | public let name: String 8 | public let createdAt: Date? 9 | public let updatedAt: Date? 10 | public var version: Int 11 | public var image: Image 12 | } 13 | 14 | extension Service: Codable {} 15 | -------------------------------------------------------------------------------- /Sources/DockerClientSwift/Models/Utilitities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Status: Codable { 4 | let status: String 5 | } 6 | 7 | public struct MessageResponse: Codable { 8 | let message: String 9 | } 10 | 11 | /// Representation of an image digest. 12 | public struct Digest { 13 | public var rawValue: String 14 | 15 | init(_ rawValue: String) { 16 | self.rawValue = rawValue 17 | } 18 | } 19 | 20 | extension Digest: ExpressibleByStringLiteral { 21 | public init(stringLiteral value: String) { 22 | self.rawValue = value 23 | } 24 | } 25 | 26 | extension Digest: Codable {} 27 | 28 | public enum DockerError: Error { 29 | case message(String) 30 | case unknownResponse(String) 31 | case errorCode(Int, String?) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Tests/DockerClientTests/ContainerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DockerClientSwift 3 | import Logging 4 | import Foundation 5 | #if canImport(FoundationNetworking) 6 | import FoundationNetworking 7 | #endif 8 | 9 | final class ContainerTests: XCTestCase { 10 | 11 | var client: DockerClient! 12 | 13 | override func setUp() { 14 | client = DockerClient.testable() 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | try! client.syncShutdown() 19 | } 20 | 21 | func testCreateContainers() throws { 22 | let image = try client.images.pullImage(byName: "hello-world", tag: "latest").wait() 23 | let container = try client.containers.createContainer(image: image).wait() 24 | 25 | XCTAssertEqual(container.command, "/hello") 26 | } 27 | 28 | func testListContainers() throws { 29 | let image = try client.images.pullImage(byName: "hello-world", tag: "latest").wait() 30 | let _ = try client.containers.createContainer(image: image).wait() 31 | 32 | let containers = try client.containers.list(all: true).wait() 33 | 34 | XCTAssert(containers.count >= 1) 35 | } 36 | 37 | func testInspectContainer() throws { 38 | let image = try client.images.pullImage(byName: "hello-world", tag: "latest").wait() 39 | let container = try client.containers.createContainer(image: image).wait() 40 | 41 | let inspectedContainer = try client.containers.get(containerByNameOrId: container.id.value).wait() 42 | 43 | XCTAssertEqual(inspectedContainer.id, container.id) 44 | XCTAssertEqual(inspectedContainer.command, "/hello") 45 | } 46 | 47 | func testStartingContainerAndRetrievingLogs() throws { 48 | let image = try client.images.pullImage(byName: "hello-world", tag: "latest").wait() 49 | let container = try client.containers.createContainer(image: image).wait() 50 | _ = try container.start(on: client).wait() 51 | let output = try container.logs(on: client).wait() 52 | // Depending on CPU architecture, step 2 of the log output may by: 53 | // 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 54 | // (amd64) 55 | // or 56 | // 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 57 | // (arm64v8) 58 | // 59 | // Just check the lines before and after this line 60 | let expectedOutputPrefix = """ 61 | 62 | Hello from Docker! 63 | This message shows that your installation appears to be working correctly. 64 | 65 | To generate this message, Docker took the following steps: 66 | 1. The Docker client contacted the Docker daemon. 67 | 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 68 | """ 69 | let expectedOutputSuffix = """ 70 | 3. The Docker daemon created a new container from that image which runs the 71 | executable that produces the output you are currently reading. 72 | 4. The Docker daemon streamed that output to the Docker client, which sent it 73 | to your terminal. 74 | 75 | To try something more ambitious, you can run an Ubuntu container with: 76 | $ docker run -it ubuntu bash 77 | 78 | Share images, automate workflows, and more with a free Docker ID: 79 | https://hub.docker.com/ 80 | 81 | For more examples and ideas, visit: 82 | https://docs.docker.com/get-started/ 83 | 84 | """ 85 | 86 | XCTAssertTrue( 87 | output.hasPrefix(expectedOutputPrefix), 88 | """ 89 | "\(output)" 90 | did not start with 91 | "\(expectedOutputPrefix)" 92 | """ 93 | ) 94 | XCTAssertTrue( 95 | output.hasSuffix(expectedOutputSuffix), 96 | """ 97 | "\(output)" 98 | did not end with 99 | "\(expectedOutputSuffix)" 100 | """ 101 | ) 102 | } 103 | 104 | func testStartingContainerForwardingToSpecificPort() throws { 105 | let image = try client.images.pullImage(byName: "nginxdemos/hello", tag: "plain-text").wait() 106 | let container = try client.containers.createContainer(image: image, portBindings: [PortBinding(hostPort: 8080, containerPort: 80)]).wait() 107 | _ = try container.start(on: client).wait() 108 | 109 | let sem: DispatchSemaphore = DispatchSemaphore(value: 0) 110 | let task = URLSession.shared.dataTask(with: URL(string: "http://localhost:8080")!) { (data, response, _) in 111 | let httpResponse = response as? HTTPURLResponse 112 | XCTAssertEqual(httpResponse?.statusCode, 200) 113 | XCTAssertEqual(httpResponse?.value(forHTTPHeaderField: "Content-Type"), "text/plain") 114 | XCTAssertTrue(String(data: data!, encoding: .utf8)!.hasPrefix("Server address")) 115 | 116 | sem.signal() 117 | } 118 | task.resume() 119 | sem.wait() 120 | try container.stop(on: client).wait() 121 | } 122 | 123 | func testStartingContainerForwardingToRandomPort() throws { 124 | let image = try client.images.pullImage(byName: "nginxdemos/hello", tag: "plain-text").wait() 125 | let container = try client.containers.createContainer(image: image, portBindings: [PortBinding(containerPort: 80)]).wait() 126 | let portBindings = try container.start(on: client).wait() 127 | let randomPort = portBindings[0].hostPort 128 | 129 | let sem: DispatchSemaphore = DispatchSemaphore(value: 0) 130 | let task = URLSession.shared.dataTask(with: URL(string: "http://localhost:\(randomPort)")!) { (data, response, _) in 131 | let httpResponse = response as? HTTPURLResponse 132 | XCTAssertEqual(httpResponse?.statusCode, 200) 133 | XCTAssertEqual(httpResponse?.value(forHTTPHeaderField: "Content-Type"), "text/plain") 134 | XCTAssertTrue(String(data: data!, encoding: .utf8)!.hasPrefix("Server address")) 135 | 136 | sem.signal() 137 | } 138 | task.resume() 139 | sem.wait() 140 | try container.stop(on: client).wait() 141 | } 142 | 143 | func testPruneContainers() throws { 144 | let image = try client.images.pullImage(byName: "nginx", tag: "latest").wait() 145 | let container = try client.containers.createContainer(image: image).wait() 146 | _ = try container.start(on: client).wait() 147 | try container.stop(on: client).wait() 148 | 149 | let pruned = try client.containers.prune().wait() 150 | 151 | let containers = try client.containers.list(all: true).wait() 152 | XCTAssert(!containers.map(\.id).contains(container.id)) 153 | XCTAssert(pruned.reclaimedSpace > 0) 154 | XCTAssert(pruned.containersIds.contains(container.id)) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Tests/DockerClientTests/ImageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DockerClientSwift 3 | import Logging 4 | 5 | final class ImageTests: XCTestCase { 6 | 7 | var client: DockerClient! 8 | 9 | override func setUp() { 10 | client = DockerClient.testable() 11 | } 12 | 13 | override func tearDownWithError() throws { 14 | try! client.syncShutdown() 15 | } 16 | 17 | func testPullImage() throws { 18 | let image = try client.images.pullImage(byName: "hello-world", tag: "latest").wait() 19 | 20 | XCTAssertTrue(image.repositoryTags.contains(where: { $0.repository == "hello-world" && $0.tag == "latest"})) 21 | } 22 | 23 | func testListImage() throws { 24 | let _ = try client.images.pullImage(byName: "hello-world", tag: "latest").wait() 25 | 26 | let images = try client.images.list().wait() 27 | 28 | XCTAssert(images.count >= 1) 29 | } 30 | 31 | func testParsingRepositoryTagSuccessfull() { 32 | let rt = Image.RepositoryTag("hello-world:latest") 33 | 34 | XCTAssertEqual(rt?.repository, "hello-world") 35 | XCTAssertEqual(rt?.tag, "latest") 36 | } 37 | 38 | func testParsingRepositoryTagThreeComponents() { 39 | let rt = Image.RepositoryTag("hello-world:latest:anotherone") 40 | 41 | XCTAssertNil(rt) 42 | } 43 | 44 | func testParsingRepositoryTagOnlyRepo() { 45 | let rt = Image.RepositoryTag("hello-world") 46 | 47 | XCTAssertEqual(rt?.repository, "hello-world") 48 | XCTAssertNil(rt?.tag) 49 | } 50 | 51 | func testParsingRepositoryTagWithDigest() { 52 | let rt = Image.RepositoryTag("sha256:89b647c604b2a436fc3aa56ab1ec515c26b085ac0c15b0d105bc475be15738fb") 53 | 54 | XCTAssertNil(rt) 55 | } 56 | 57 | func testInspectImage() throws { 58 | XCTAssertNoThrow(try client.images.get(imageByNameOrId: "nginx:latest").wait()) 59 | } 60 | 61 | func testPruneContainers() throws { 62 | let image = try client.images.pullImage(byName: "nginx", tag: "1.18-alpine").wait() 63 | 64 | let pruned = try client.images.prune(all: true).wait() 65 | 66 | let images = try client.images.list().wait() 67 | XCTAssert(!images.map(\.id).contains(image.id)) 68 | XCTAssert(pruned.reclaimedSpace > 0) 69 | XCTAssert(pruned.imageIds.contains(image.id)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/DockerClientTests/ServiceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DockerClientSwift 3 | import Logging 4 | 5 | final class ServiceTests: XCTestCase { 6 | 7 | var client: DockerClient! 8 | 9 | override func setUp() { 10 | client = DockerClient.testable() 11 | } 12 | 13 | override func tearDownWithError() throws { 14 | try! client.syncShutdown() 15 | // Remove all services in a Docker system `docker service ls -q | xargs echo` 16 | } 17 | 18 | func testListingServices() throws { 19 | let name = UUID().uuidString 20 | let _ = try client.services.create(serviceName: name, image: Image(id: .init("nginx:alpine"))).wait() 21 | let services = try client.services.list().wait() 22 | 23 | XCTAssert(services.count >= 1) 24 | } 25 | 26 | func testUpdateService() throws { 27 | let name = UUID().uuidString 28 | let service = try client.services.create(serviceName: name, image: Image(id: .init("nginx:alpine"))).wait() 29 | let updatedService = try client.services.update(service: service, newImage: Image(id: "nginx:latest")).wait() 30 | 31 | XCTAssertTrue(updatedService.version > service.version) 32 | } 33 | 34 | func testInspectService() throws { 35 | let name = UUID().uuidString 36 | let service = try client.services.create(serviceName: name, image: Image(id: .init("nginx:alpine"))).wait() 37 | XCTAssertNoThrow(try client.services.get(serviceByNameOrId: service.id.value)) 38 | XCTAssertEqual(service.name, name) 39 | } 40 | 41 | func testCreateService() throws { 42 | let name = UUID().uuidString 43 | let service = try client.services.create(serviceName: name, image: Image(id: .init("nginx:latest"))).wait() 44 | 45 | XCTAssertEqual(service.name, name) 46 | } 47 | 48 | func testParsingDate() { 49 | XCTAssertNotNil(Date.parseDockerDate("2021-03-12T12:34:10.239624085Z")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/DockerClientTests/SystemTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DockerClientSwift 3 | import Logging 4 | 5 | final class SystemTests: XCTestCase { 6 | var client: DockerClient! 7 | 8 | override func setUp() { 9 | client = DockerClient.testable() 10 | } 11 | 12 | override func tearDownWithError() throws { 13 | try! client.syncShutdown() 14 | } 15 | 16 | func testDockerVersion() throws { 17 | XCTAssertNoThrow(try client.version().wait()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/DockerClientTests/Utils/DockerClient+Testable.swift: -------------------------------------------------------------------------------- 1 | import DockerClientSwift 2 | import Logging 3 | 4 | extension DockerClient { 5 | /// Creates a new `DockerClient` instance that can be used for testing. 6 | /// It creates a new `Logger` with the log level `.debug` and passes it to the `DockerClient`. 7 | /// - Returns: Returns a `DockerClient` that is meant for testing purposes. 8 | static func testable() -> DockerClient { 9 | var logger = Logger(label: "docker-client-tests") 10 | logger.logLevel = .debug 11 | return DockerClient(logger: logger) 12 | } 13 | } 14 | --------------------------------------------------------------------------------