mirror of
https://github.com/apple/container.git
synced 2026-09-10 17:55:38 +00:00
feat: add proxy utility (#288)
Signed-off-by: Harry Li <harrymingh@gmail.com> Co-authored-by: Dmitry Kovba <dkovba@apple.com>
This commit is contained in:
@@ -45,7 +45,6 @@ public struct ContainerManager: Sendable {
|
||||
@available(macOS 26.0, *)
|
||||
public struct VmnetNetwork: Network {
|
||||
private var allocator: Allocator
|
||||
// `reference` isn't used concurrently.
|
||||
nonisolated(unsafe) private let reference: vmnet_network_ref
|
||||
|
||||
/// The IPv4 subnet of this network.
|
||||
@@ -95,7 +94,6 @@ public struct ContainerManager: Sendable {
|
||||
public let gateway: String?
|
||||
public let macAddress: String?
|
||||
|
||||
// `reference` isn't used concurrently.
|
||||
nonisolated(unsafe) private let reference: vmnet_network_ref
|
||||
|
||||
public init(
|
||||
@@ -480,4 +478,9 @@ extension CIDRAddress {
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private struct SendableReference: Sendable {
|
||||
nonisolated(unsafe) private let reference: vmnet_network_ref
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -24,14 +24,14 @@ import Crypto
|
||||
import Foundation
|
||||
|
||||
extension ImageStore {
|
||||
public struct ExportOperation: Sendable {
|
||||
internal struct ExportOperation {
|
||||
let name: String
|
||||
let tag: String
|
||||
let contentStore: ContentStore
|
||||
let client: ContentClient
|
||||
let progress: ProgressHandler?
|
||||
|
||||
public init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) {
|
||||
init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) {
|
||||
self.contentStore = contentStore
|
||||
self.client = client
|
||||
self.progress = progress
|
||||
@@ -40,7 +40,7 @@ extension ImageStore {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
|
||||
internal func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
|
||||
var pushQueue: [[Descriptor]] = []
|
||||
var current: [Descriptor] = [index]
|
||||
while !current.isEmpty {
|
||||
|
||||
@@ -22,7 +22,7 @@ import ContainerizationOCI
|
||||
import Foundation
|
||||
|
||||
extension ImageStore {
|
||||
public struct ImportOperation: Sendable {
|
||||
internal struct ImportOperation {
|
||||
static let decoder = JSONDecoder()
|
||||
|
||||
let client: ContentClient
|
||||
@@ -31,7 +31,7 @@ extension ImageStore {
|
||||
let progress: ProgressHandler?
|
||||
let name: String
|
||||
|
||||
public init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) {
|
||||
init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) {
|
||||
self.client = client
|
||||
self.ingestDir = ingestDir
|
||||
self.contentStore = contentStore
|
||||
@@ -40,7 +40,7 @@ extension ImageStore {
|
||||
}
|
||||
|
||||
/// Pull the required image layers for the provided descriptor and platform(s) into the given directory using the provided client. Returns a descriptor to the Index manifest.
|
||||
public func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor {
|
||||
internal func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor {
|
||||
var toProcess = [root]
|
||||
while !toProcess.isEmpty {
|
||||
// Count the total number of blobs and their size
|
||||
@@ -123,14 +123,14 @@ extension ImageStore {
|
||||
for _ in 0..<8 {
|
||||
if let desc = iterator.next() {
|
||||
group.addTask {
|
||||
try await self.fetch(desc)
|
||||
try await fetch(desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
for try await _ in group {
|
||||
if let desc = iterator.next() {
|
||||
group.addTask {
|
||||
try await self.fetch(desc)
|
||||
try await fetch(desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ public final class NATNetworkInterface: Interface, Sendable {
|
||||
public let macAddress: String?
|
||||
|
||||
@available(macOS 26, *)
|
||||
// `reference` isn't used concurrently.
|
||||
public nonisolated(unsafe) let reference: vmnet_network_ref!
|
||||
|
||||
@available(macOS 26, *)
|
||||
|
||||
@@ -267,7 +267,6 @@ extension SocketRelay {
|
||||
)
|
||||
}
|
||||
|
||||
// `buf1` isn't used concurrently.
|
||||
nonisolated(unsafe) let buf1 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
|
||||
connSource.setEventHandler {
|
||||
Self.fdCopyHandler(
|
||||
@@ -279,7 +278,6 @@ extension SocketRelay {
|
||||
}
|
||||
|
||||
nonisolated(unsafe) let buf2 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
|
||||
// `buf2` isn't used concurrently.
|
||||
vsockConnectionSource.setEventHandler {
|
||||
Self.fdCopyHandler(
|
||||
buffer: buf2,
|
||||
|
||||
@@ -68,7 +68,6 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// `vm` isn't used concurrently.
|
||||
private nonisolated(unsafe) let vm: VZVirtualMachine
|
||||
private let queue: DispatchQueue
|
||||
private let group: MultiThreadedEventLoopGroup
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A small utility to resolve proxy settings (HTTP(S)_PROXY / NO_PROXY).
|
||||
public enum ProxyUtils {
|
||||
/// Resolves the proxy URL for a given host based on environment variables.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - host: The target hostname (without scheme).
|
||||
/// - env: Optional environment dictionary; defaults to process environment.
|
||||
/// - Returns: The proxy URL to use, or `nil` for direct connection.
|
||||
public static func proxy(for host: String, env: [String: String]? = nil) -> URL? {
|
||||
let env = env ?? ProcessInfo.processInfo.environment
|
||||
|
||||
// Case-insensitive lookup for both upper/lower keys
|
||||
let httpProxy = env["HTTP_PROXY"] ?? env["http_proxy"]
|
||||
let httpsProxy = env["HTTPS_PROXY"] ?? env["https_proxy"]
|
||||
|
||||
let noProxy = env["NO_PROXY"] ?? env["no_proxy"]
|
||||
|
||||
// If NO_PROXY matches → skip proxy
|
||||
if let noProxy, shouldBypassProxy(host: host, noProxy: noProxy) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prefer HTTPS proxy if set, otherwise fall back to HTTP proxy
|
||||
let proxyStr = httpsProxy ?? httpProxy
|
||||
guard let proxyStr, let url = URL(string: proxyStr) else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/// Check if a host should bypass proxy according to NO_PROXY.
|
||||
/// - Example: NO_PROXY=".example.com,localhost,127.0.0.1"
|
||||
private static func shouldBypassProxy(host: String, noProxy: String) -> Bool {
|
||||
let entries = noProxy.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
for entry in entries {
|
||||
if entry.isEmpty { continue }
|
||||
if entry == "*" { return true }
|
||||
if host == entry { return true }
|
||||
if entry.hasPrefix(".") && host.hasSuffix(entry) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ public final class AsyncSignalHandler: Sendable {
|
||||
|
||||
struct State: Sendable {
|
||||
var conts: [AsyncStream<Int32>.Continuation] = []
|
||||
// `sources` isn't used concurrently.
|
||||
nonisolated(unsafe) var sources: [any DispatchSourceSignal] = []
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ extension IntegrationSuite {
|
||||
}
|
||||
|
||||
final class BufferWriter: Writer {
|
||||
// `data` isn't used concurrently.
|
||||
nonisolated(unsafe) var data = Data()
|
||||
|
||||
func write(_ data: Data) throws {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
struct ProxyUtilsTests {
|
||||
|
||||
@Test("HTTP proxy resolution")
|
||||
func testHttpProxy() {
|
||||
let env = ["http_proxy": "http://proxy.local:8080"]
|
||||
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
|
||||
#expect(proxy?.absoluteString == "http://proxy.local:8080")
|
||||
}
|
||||
|
||||
@Test("HTTPS proxy resolution")
|
||||
func testHttpsProxy() {
|
||||
let env = ["https_proxy": "https://secureproxy.local:8443"]
|
||||
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
|
||||
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
|
||||
}
|
||||
|
||||
@Test("NO_PROXY exact match")
|
||||
func testNoProxyExactMatch() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"NO_PROXY": "example.com",
|
||||
]
|
||||
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
|
||||
#expect(proxy == nil)
|
||||
}
|
||||
|
||||
@Test("Uppercase HTTP_PROXY is respected")
|
||||
func testUppercaseHttpProxy() {
|
||||
let env = ["HTTP_PROXY": "http://upper.local:8081"]
|
||||
let proxy = ProxyUtils.proxy(for: "upper.com", env: env)
|
||||
#expect(proxy?.absoluteString == "http://upper.local:8081")
|
||||
}
|
||||
|
||||
@Test("Lowercase no_proxy is respected")
|
||||
func testLowercaseNoProxy() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"no_proxy": "lower.com",
|
||||
]
|
||||
let proxy = ProxyUtils.proxy(for: "lower.com", env: env)
|
||||
#expect(proxy == nil)
|
||||
}
|
||||
|
||||
@Test("HTTPS proxy has higher priority than HTTP proxy")
|
||||
func testHttpsPreferredOverHttp() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"https_proxy": "https://secureproxy.local:8443",
|
||||
]
|
||||
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
|
||||
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
|
||||
}
|
||||
|
||||
@Test("Uppercase HTTP_PROXY overrides lowercase http_proxy")
|
||||
func testUppercaseOverridesLowercaseHttp() {
|
||||
let env = [
|
||||
"http_proxy": "http://lower.local:8080",
|
||||
"HTTP_PROXY": "http://upper.local:8081",
|
||||
]
|
||||
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
|
||||
#expect(proxy?.absoluteString == "http://upper.local:8081")
|
||||
}
|
||||
|
||||
@Test("Uppercase HTTPS_PROXY overrides lowercase https_proxy")
|
||||
func testUppercaseOverridesLowercaseHttps() {
|
||||
let env = [
|
||||
"https_proxy": "https://lower.local:8443",
|
||||
"HTTPS_PROXY": "https://upper.local:8444",
|
||||
]
|
||||
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
|
||||
#expect(proxy?.absoluteString == "https://upper.local:8444")
|
||||
}
|
||||
|
||||
@Test("Uppercase NO_PROXY overrides lowercase no_proxy")
|
||||
func testUppercaseOverridesLowercaseNoProxy() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"no_proxy": "foo.com",
|
||||
"NO_PROXY": "bar.com",
|
||||
]
|
||||
let proxyFoo = ProxyUtils.proxy(for: "foo.com", env: env)
|
||||
let proxyBar = ProxyUtils.proxy(for: "bar.com", env: env)
|
||||
|
||||
#expect(proxyFoo != nil)
|
||||
#expect(proxyBar == nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user