mirror of
https://github.com/apple/container.git
synced 2026-07-13 04:57:08 +00:00
0846bb6640
Its a bit weird to have to pass `--http` when pulling / pushing images if we are trying to do so from a local registry This PR adds a `RequestScheme` enum type which tries to detect if the connection to the registry should be over http or https. This auto detection is the default behavior and can be overridden by passing the `--scheme http/https` flag to the `run` ,`pull`, `push` `login` and `registry default` command The auto detection works by looking at the hostname / IP. HTTP is used in the following cases - If the hostname starts with `localhost` or is `127.x.x.x` - if the host looks like an IP from the private address space - If the host ends with the current default dns domain name Also in this PR: - Removing some used CLI flag groups and TODOs - Better error messages when registry credentials are not found for a host Signed-off-by: Aditya Ramani <a_ramani@apple.com>
212 lines
9.1 KiB
Swift
212 lines
9.1 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the container 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 ContainerClient
|
|
import ContainerImagesServiceClient
|
|
import Containerization
|
|
import ContainerizationArchive
|
|
import ContainerizationError
|
|
import ContainerizationExtras
|
|
import ContainerizationOCI
|
|
import Foundation
|
|
import Logging
|
|
import TerminalProgress
|
|
|
|
public actor ImagesService {
|
|
public static let keychainID = "com.apple.container"
|
|
|
|
private let log: Logger
|
|
private let contentStore: ContentStore
|
|
private let imageStore: ImageStore
|
|
private let snapshotStore: SnapshotStore
|
|
|
|
public init(contentStore: ContentStore, imageStore: ImageStore, snapshotStore: SnapshotStore, log: Logger) throws {
|
|
self.contentStore = contentStore
|
|
self.imageStore = imageStore
|
|
self.snapshotStore = snapshotStore
|
|
self.log = log
|
|
}
|
|
|
|
private func _list() async throws -> [Containerization.Image] {
|
|
try await imageStore.list()
|
|
}
|
|
|
|
private func _get(_ reference: String) async throws -> Containerization.Image {
|
|
try await imageStore.get(reference: reference)
|
|
}
|
|
|
|
private func _get(_ description: ImageDescription) async throws -> Containerization.Image {
|
|
let exists = try await self._get(description.reference)
|
|
guard exists.descriptor == description.descriptor else {
|
|
throw ContainerizationError(.invalidState, message: "Descriptor mismatch. Expected \(description.descriptor), got \(exists.descriptor)")
|
|
}
|
|
return exists
|
|
}
|
|
|
|
public func list() async throws -> [ImageDescription] {
|
|
self.log.info("ImagesService: \(#function)")
|
|
return try await imageStore.list().map { $0.description.fromCZ }
|
|
}
|
|
|
|
public func pull(reference: String, platform: Platform?, insecure: Bool, progressUpdate: ProgressUpdateHandler?) async throws -> ImageDescription {
|
|
self.log.info("ImagesService: \(#function) - ref: \(reference), platform: \(String(describing: platform)), insecure: \(insecure)")
|
|
let img = try await Self.withAuthentication(ref: reference) { auth in
|
|
try await self.imageStore.pull(
|
|
reference: reference, platform: platform, insecure: insecure, auth: auth, progress: ContainerizationProgressAdapter.handler(from: progressUpdate))
|
|
}
|
|
guard let img else {
|
|
throw ContainerizationError(.internalError, message: "Failed to pull image \(reference)")
|
|
}
|
|
return img.description.fromCZ
|
|
}
|
|
|
|
public func push(reference: String, platform: Platform?, insecure: Bool, progressUpdate: ProgressUpdateHandler?) async throws {
|
|
self.log.info("ImagesService: \(#function) - ref: \(reference), platform: \(String(describing: platform)), insecure: \(insecure)")
|
|
try await Self.withAuthentication(ref: reference) { auth in
|
|
try await self.imageStore.push(
|
|
reference: reference, platform: platform, insecure: insecure, auth: auth, progress: ContainerizationProgressAdapter.handler(from: progressUpdate))
|
|
}
|
|
}
|
|
|
|
public func tag(old: String, new: String) async throws -> ImageDescription {
|
|
self.log.info("ImagesService: \(#function) - old: \(old), new: \(new)")
|
|
let img = try await self.imageStore.tag(existing: old, new: new)
|
|
return img.description.fromCZ
|
|
}
|
|
|
|
public func delete(reference: String, garbageCollect: Bool) async throws {
|
|
self.log.info("ImagesService: \(#function) - ref: \(reference)")
|
|
try await self.imageStore.delete(reference: reference, performCleanup: garbageCollect)
|
|
}
|
|
|
|
public func save(reference: String, out: URL, platform: Platform?) async throws {
|
|
self.log.info("ImagesService: \(#function) - reference: \(reference) , platform: \(String(describing: platform))")
|
|
let tempDir = FileManager.default.uniqueTemporaryDirectory()
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
try await self.imageStore.save(references: [reference], out: tempDir, platform: platform)
|
|
let writer = try ArchiveWriter(format: .pax, filter: .none, file: out)
|
|
try writer.archiveDirectory(tempDir)
|
|
try writer.finishEncoding()
|
|
}
|
|
|
|
public func load(from tarFile: URL) async throws -> [ImageDescription] {
|
|
self.log.info("ImagesService: \(#function) from: \(tarFile.absolutePath())")
|
|
let reader = try ArchiveReader(file: tarFile)
|
|
let tempDir = FileManager.default.uniqueTemporaryDirectory()
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
try reader.extractContents(to: tempDir)
|
|
let loaded = try await self.imageStore.load(from: tempDir)
|
|
var images: [ImageDescription] = []
|
|
for image in loaded {
|
|
images.append(image.description.fromCZ)
|
|
}
|
|
return images
|
|
}
|
|
|
|
public func prune() async throws -> ([String], UInt64) {
|
|
let images = try await self._list()
|
|
let freedSnapshotBytes = try await self.snapshotStore.clean(keepingSnapshotsFor: images)
|
|
let (deleted, freedContentBytes) = try await self.imageStore.prune()
|
|
return (deleted, freedContentBytes + freedSnapshotBytes)
|
|
}
|
|
}
|
|
|
|
// MARK: Image Snapshot Methods
|
|
|
|
extension ImagesService {
|
|
public func unpack(description: ImageDescription, platform: Platform?, progressUpdate: ProgressUpdateHandler?) async throws {
|
|
self.log.info("ImagesService: \(#function) - description: \(description), platform: \(String(describing: platform))")
|
|
let img = try await self._get(description)
|
|
try await self.snapshotStore.unpack(image: img, platform: platform, progressUpdate: progressUpdate)
|
|
}
|
|
|
|
public func deleteImageSnapshot(description: ImageDescription, platform: Platform?) async throws {
|
|
self.log.info("ImagesService: \(#function) - description: \(description), platform: \(String(describing: platform))")
|
|
let img = try await self._get(description)
|
|
try await self.snapshotStore.delete(for: img, platform: platform)
|
|
}
|
|
|
|
public func getImageSnapshot(description: ImageDescription, platform: Platform) async throws -> Filesystem {
|
|
self.log.info("ImagesService: \(#function) - description: \(description), platform: \(String(describing: platform))")
|
|
let img = try await self._get(description)
|
|
return try await self.snapshotStore.get(for: img, platform: platform)
|
|
}
|
|
}
|
|
|
|
// MARK: Static Methods
|
|
|
|
extension ImagesService {
|
|
private static func withAuthentication<T>(
|
|
ref: String, _ body: @Sendable @escaping (_ auth: Authentication?) async throws -> T?
|
|
) async throws -> T? {
|
|
var authentication: Authentication?
|
|
let ref = try Reference.parse(ref)
|
|
guard let host = ref.resolvedDomain else {
|
|
throw ContainerizationError(.invalidArgument, message: "No host specified in image reference: \(ref)")
|
|
}
|
|
authentication = Self.authenticationFromEnv(host: host)
|
|
if let authentication {
|
|
return try await body(authentication)
|
|
}
|
|
let keychain = KeychainHelper(id: Self.keychainID)
|
|
authentication = try? keychain.lookup(domain: host)
|
|
do {
|
|
return try await body(authentication)
|
|
} catch let err as RegistryClient.Error {
|
|
guard case .invalidStatus(_, let status) = err else {
|
|
throw err
|
|
}
|
|
guard status == .unauthorized || status == .forbidden else {
|
|
throw err
|
|
}
|
|
guard authentication != nil else {
|
|
throw ContainerizationError(.internalError, message: "\(String(describing: err)). No credentials found for host \(host)")
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
private static func authenticationFromEnv(host: String) -> Authentication? {
|
|
let env = ProcessInfo.processInfo.environment
|
|
guard env["CONTAINER_REGISTRY_HOST"] == host else {
|
|
return nil
|
|
}
|
|
guard let user = env["CONTAINER_REGISTRY_USER"], let password = env["CONTAINER_REGISTRY_TOKEN"] else {
|
|
return nil
|
|
}
|
|
return BasicAuthentication(username: user, password: password)
|
|
}
|
|
}
|
|
|
|
extension ImageDescription {
|
|
public var toCZ: Containerization.Image.Description {
|
|
.init(reference: self.reference, descriptor: self.descriptor)
|
|
}
|
|
}
|
|
|
|
extension Containerization.Image.Description {
|
|
public var fromCZ: ImageDescription {
|
|
.init(
|
|
reference: self.reference,
|
|
descriptor: self.descriptor
|
|
)
|
|
}
|
|
}
|