Skip to content

Commit

Permalink
Support Docker Helpers (#205)
Browse files Browse the repository at this point in the history
Fixes #167
  • Loading branch information
fkorotkov authored Aug 29, 2022
1 parent 048a550 commit c54b140
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Sources/tart/Commands/Login.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ struct Login: AsyncParsableCommand {

do {
let registry = try Registry(host: host, namespace: "", insecure: insecure,
credentialsProvider: credentialsProvider)
credentialsProviders: [credentialsProvider])
try await registry.ping()
} catch {
print("invalid credentials: \(error)")
Expand Down
63 changes: 63 additions & 0 deletions Sources/tart/Credentials/HelperProgramCredentialsProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation

class HelperProgramCredentialsProvider: CredentialsProvider {
func retrieve(host: String) throws -> (String, String)? {
let dockerConfigURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".docker").appendingPathComponent("config.json")
if !FileManager.default.fileExists(atPath: dockerConfigURL.path) {
return nil
}
let config = try JSONDecoder().decode(DockerConfig.self, from: Data(contentsOf: dockerConfigURL))

if let helperProgram = config.credHelpers[host] {
return try executeHelper(binaryName: "docker-credential-\(helperProgram)", host: host)
}

return nil
}

private func executeHelper(binaryName: String, host: String) throws -> (String, String)? {
guard let executableURL = resolveBinaryPath(binaryName) else {
throw CredentialsProviderError.Failed(message: "\(binaryName) not found in PATH")
}

let process = Process.init()
process.executableURL = executableURL
process.arguments = ["get"]

let outPipe = Pipe()
let inPipe = Pipe()

process.standardOutput = outPipe
process.standardError = outPipe
process.standardInput = inPipe

process.launch()

inPipe.fileHandleForWriting.write("\(host)\n".data(using: .utf8)!)
inPipe.fileHandleForWriting.closeFile()

process.waitUntilExit()

if !(process.terminationReason == .exit && process.terminationStatus == 0) {
throw CredentialsProviderError.Failed(message: "Docker helper failed!")
}

let getOutput = try JSONDecoder().decode(
DockerGetOutput.self, from: outPipe.fileHandleForReading.readDataToEndOfFile()
)
return (getOutput.Username, getOutput.Secret)
}

func store(host: String, user: String, password: String) throws {
throw CredentialsProviderError.Failed(message: "Docker helpers don't support storing!")
}
}

struct DockerConfig: Codable {
var credHelpers: Dictionary<String, String> = Dictionary()
}

struct DockerGetOutput: Codable {
var Username: String
var Secret: String
}
23 changes: 16 additions & 7 deletions Sources/tart/OCI/Registry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,29 @@ class Registry {

let baseURL: URL
let namespace: String
let credentialsProvider: CredentialsProvider
let credentialsProviders: [CredentialsProvider]

var currentAuthToken: Authentication? = nil

init(urlComponents: URLComponents,
namespace: String,
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
credentialsProviders: [CredentialsProvider] = [HelperProgramCredentialsProvider(), KeychainCredentialsProvider()]
) throws {
baseURL = urlComponents.url!
self.namespace = namespace
self.credentialsProvider = credentialsProvider
self.credentialsProviders = credentialsProviders
}

convenience init(
host: String,
namespace: String,
insecure: Bool = false,
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
credentialsProviders: [CredentialsProvider] = [HelperProgramCredentialsProvider(), KeychainCredentialsProvider()]
) throws {
let proto = insecure ? "http" : "https"
let baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!

try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProvider: credentialsProvider)
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProviders: credentialsProviders)
}

func ping() async throws {
Expand Down Expand Up @@ -303,7 +303,7 @@ class Registry {
let wwwAuthenticate = try WWWAuthenticate(rawHeaderValue: wwwAuthenticateRaw)

if wwwAuthenticate.scheme == "Basic" {
if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
if let (user, password) = try lookupCredentials(host: baseURL.host!) {
currentAuthToken = BasicAuthentication(user: user, password: password)
}

Expand Down Expand Up @@ -340,7 +340,7 @@ class Registry {

var headers: Dictionary<String, String> = Dictionary()

if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
if let (user, password) = try lookupCredentials(host: baseURL.host!) {
let encodedCredentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
headers["Authorization"] = "Basic \(encodedCredentials!)"
}
Expand All @@ -356,6 +356,15 @@ class Registry {
currentAuthToken = try TokenResponse.parse(fromData: bodyData)
}

private func lookupCredentials(host: String) throws -> (String, String)? {
for provider in credentialsProviders {
if let (user, password) = try provider.retrieve(host: host) {
return (user, password)
}
}
return nil
}

private func authAwareRequest(request: HTTPClientRequest) async throws -> HTTPClientResponse {
var request = request

Expand Down
19 changes: 1 addition & 18 deletions Sources/tart/Softnet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Softnet {
init(vmMACAddress: String) throws {
let binaryName = "softnet"

guard let executableURL = Self.resolveBinaryPath(binaryName) else {
guard let executableURL = resolveBinaryPath(binaryName) else {
throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH")
}

Expand Down Expand Up @@ -43,23 +43,6 @@ class Softnet {
process.waitUntilExit()
}

private static func resolveBinaryPath(_ name: String) -> URL? {
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
return nil
}

for pathComponent in path.split(separator: ":") {
let url = URL(fileURLWithPath: String(pathComponent))
.appendingPathComponent(name, isDirectory: false)

if FileManager.default.fileExists(atPath: url.path) {
return url
}
}

return nil
}

private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
var option_value = sizeBytes
let option_len = socklen_t(MemoryLayout<Int>.size)
Expand Down
17 changes: 17 additions & 0 deletions Sources/tart/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,20 @@ extension Collection {
indices.contains(index) ? self[index] : nil
}
}

func resolveBinaryPath(_ name: String) -> URL? {
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
return nil
}

for pathComponent in path.split(separator: ":") {
let url = URL(fileURLWithPath: String(pathComponent))
.appendingPathComponent(name, isDirectory: false)

if FileManager.default.fileExists(atPath: url.path) {
return url
}
}

return nil
}

0 comments on commit c54b140

Please sign in to comment.