From f8a4a3960ee620d4645bf2ec811fe7958efb0f15 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 18 Mar 2026 23:14:08 +0100 Subject: [PATCH 1/6] renew token from token file before it expires --- .../Config/AuthInfo+Authentication.swift | 240 ++++++++-------- .../Config/KubeConfig+Loaders.swift | 258 +++++++++--------- .../Config/KubernetesClientConfig.swift | 175 ++++++++---- 3 files changed, 367 insertions(+), 306 deletions(-) diff --git a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift index be04ecc..fd88f30 100644 --- a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift +++ b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift @@ -33,91 +33,83 @@ import Foundation import Logging import NIOSSL -public extension AuthInfo { - - func authentication(logger: Logger?) -> KubernetesClientAuthentication? { - if let username = username, let password = password { - return .basicAuth(username: username, password: password) - } - - if let token = token { - return .bearer(token: token) - } - - do { - if let tokenFile = tokenFile { - let fileURL = URL(fileURLWithPath: tokenFile) - let token = try String(contentsOf: fileURL, encoding: .utf8) - return .bearer(token: token) - } - } catch { - logger?.warning( - "Error initializing authentication from token file \(String(describing: tokenFile)): \(error)" - ) - } - - do { - if let clientCertificateFile = clientCertificate, let clientKeyFile = clientKey { - let clientCertificate = try NIOSSLCertificate( - file: clientCertificateFile, - format: .pem - ) - let clientKey = try NIOSSLPrivateKey( - file: clientKeyFile, - format: .pem - ) - return .x509( - clientCertificate: clientCertificate, - clientKey: clientKey - ) - } - - if let clientCertificateData = clientCertificateData, let clientKeyData = clientKeyData { - let clientCertificate = try NIOSSLCertificate( - bytes: [UInt8](clientCertificateData), - format: .pem - ) - let clientKey = try NIOSSLPrivateKey( - bytes: [UInt8](clientKeyData), - format: .pem - ) - return .x509( - clientCertificate: clientCertificate, - clientKey: clientKey - ) - } - } catch { - logger?.warning( - "Error initializing authentication from client certificate: \(error)" - ) - } - - #if os(Linux) || os(macOS) - do { - if let exec { - let outputData = try run( - command: exec.command, - arguments: exec.args - ) - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - let credential = try decoder.decode( - ExecCredential.self, - from: outputData - ) - - return .bearer(token: credential.status.token) - } - } catch { - logger?.warning( - "Error initializing authentication from exec \(error)" - ) - } - #endif - - return nil - } +extension AuthInfo { + + public func authentication(logger: Logger?) -> KubernetesClientAuthentication? { + if let username = username, let password = password { + return .basicAuth(username: username, password: password) + } + + if let token = token { + return .bearer(token: token) + } + + if let tokenFile = tokenFile { + return .tokenFile(source: CachedFileTokenSource(path: tokenFile)) + } + + do { + if let clientCertificateFile = clientCertificate, let clientKeyFile = clientKey { + let clientCertificate = try NIOSSLCertificate( + file: clientCertificateFile, + format: .pem + ) + let clientKey = try NIOSSLPrivateKey( + file: clientKeyFile, + format: .pem + ) + return .x509( + clientCertificate: clientCertificate, + clientKey: clientKey + ) + } + + if let clientCertificateData = clientCertificateData, let clientKeyData = clientKeyData { + let clientCertificate = try NIOSSLCertificate( + bytes: [UInt8](clientCertificateData), + format: .pem + ) + let clientKey = try NIOSSLPrivateKey( + bytes: [UInt8](clientKeyData), + format: .pem + ) + return .x509( + clientCertificate: clientCertificate, + clientKey: clientKey + ) + } + } catch { + logger?.warning( + "Error initializing authentication from client certificate: \(error)" + ) + } + + #if os(Linux) || os(macOS) + do { + if let exec { + let outputData = try run( + command: exec.command, + arguments: exec.args + ) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let credential = try decoder.decode( + ExecCredential.self, + from: outputData + ) + + return .bearer(token: credential.status.token) + } + } catch { + logger?.warning( + "Error initializing authentication from exec \(error)" + ) + } + #endif + + return nil + } } // MARK: - ExecCredential @@ -126,50 +118,50 @@ public extension AuthInfo { // Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ // ExecCredential.Spec.interactive is required as long as the ones in the Status object. public struct ExecCredential: Codable { - let apiVersion: String - let kind: String - let spec: Spec - let status: Status + let apiVersion: String + let kind: String + let spec: Spec + let status: Status } -public extension ExecCredential { +extension ExecCredential { - struct Spec: Codable { - let cluster: Cluster? - let interactive: Bool? - } + public struct Spec: Codable { + let cluster: Cluster? + let interactive: Bool? + } - struct Status: Codable { - let expirationTimestamp: Date - let token: String - let clientCertificateData: String? - let clientKeyData: String? - } + public struct Status: Codable { + let expirationTimestamp: Date + let token: String + let clientCertificateData: String? + let clientKeyData: String? + } } #if os(Linux) || os(macOS) - internal func run(command: String, arguments: [String]? = nil) throws -> Data { - func run(_ command: String, _ arguments: [String]?) throws -> Data { - let task = Process() - task.executableURL = URL(fileURLWithPath: command) - arguments.flatMap { task.arguments = $0 } - - let pipe = Pipe() - task.standardOutput = pipe - - try task.run() - - return pipe.fileHandleForReading.availableData - } - - func resolve(command: String) throws -> String { - try String( - decoding: - run("/usr/bin/which", ["\(command)"]), - as: UTF8.self - ).trimmingCharacters(in: .whitespacesAndNewlines) - } - - return try run(resolve(command: command), arguments) - } + internal func run(command: String, arguments: [String]? = nil) throws -> Data { + func run(_ command: String, _ arguments: [String]?) throws -> Data { + let task = Process() + task.executableURL = URL(fileURLWithPath: command) + arguments.flatMap { task.arguments = $0 } + + let pipe = Pipe() + task.standardOutput = pipe + + try task.run() + + return pipe.fileHandleForReading.availableData + } + + func resolve(command: String) throws -> String { + try String( + decoding: + run("/usr/bin/which", ["\(command)"]), + as: UTF8.self + ).trimmingCharacters(in: .whitespacesAndNewlines) + } + + return try run(resolve(command: command), arguments) + } #endif diff --git a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift index 27b20f5..8e6495e 100644 --- a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift +++ b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift @@ -18,136 +18,146 @@ import Foundation import Logging import Yams -public extension KubeConfig { - - static func from(config: String) throws -> KubeConfig { - let decoder = YAMLDecoder() - return try decoder.decode(KubeConfig.self, from: config) - } - - static func from(url: URL) throws -> KubeConfig { - let contents = try String(contentsOf: url, encoding: .utf8) - - return try from(config: contents) - } - - #if os(Linux) || os(macOS) - static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws -> KubeConfig? { - guard let varContent = ProcessInfo.processInfo.environment[envVar] else { - logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") - return nil - } - - let expanded = varContent.stringByExpandingTildePath() - let kubeConfigURL = URL(fileURLWithPath: expanded) - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) - } - #endif - - #if os(Linux) || os(macOS) - static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { - guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { - logger?.info("Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set.") - return nil - } - - let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) - } - #endif - - #if os(Linux) || os(macOS) - static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { - guard - let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], - let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] - else { - logger?.warning("Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set") - return nil - } - - let apiServerUrl = if host.contains(":") { - "https://[\(host)]:\(port)" - } else { - "https://\(host):\(port)" - } - - let tokenFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/token") - guard let token = try? String(contentsOf: tokenFile, encoding: .utf8) else { - logger?.warning("Did not find service account token at /var/run/secrets/kubernetes.io/serviceaccount/token") - return nil - } - - let namespaceFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace") - let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) - if namespace == nil { - logger?.debug("Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace") - } - - let certificateAuthorityFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") - let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) - - return KubeConfig( - kind: "Config", - apiVersion: "v1", - clusters: [ - NamedCluster( - name: "kubernetes-cluster-local", - cluster: Cluster( - server: apiServerUrl, - insecureSkipTLSVerify: certificateAuthorityData == nil, - certificateAuthorityData: certificateAuthorityData - ) - ), - ], - users: [ - NamedAuthInfo( - name: "service-account-user", - authInfo: AuthInfo( - token: token - ) - ), - ], - contexts: [ - NamedContext( - name: "service-account-context", - context: Context( - cluster: "kubernetes-cluster-local", - user: "service-account-user", - namespace: namespace - ) - ), - ], - currentContext: "service-account-context" - ) - } - #endif +extension KubeConfig { + + public static func from(config: String) throws -> KubeConfig { + let decoder = YAMLDecoder() + return try decoder.decode(KubeConfig.self, from: config) + } + + public static func from(url: URL) throws -> KubeConfig { + let contents = try String(contentsOf: url, encoding: .utf8) + + return try from(config: contents) + } + + #if os(Linux) || os(macOS) + public static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws + -> KubeConfig? + { + guard let varContent = ProcessInfo.processInfo.environment[envVar] else { + logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") + return nil + } + + let expanded = varContent.stringByExpandingTildePath() + let kubeConfigURL = URL(fileURLWithPath: expanded) + logger?.info("Loading configuration from \(kubeConfigURL)") + + return try from(url: kubeConfigURL) + } + #endif + + #if os(Linux) || os(macOS) + public static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { + guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { + logger?.info( + "Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set.") + return nil + } + + let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") + logger?.info("Loading configuration from \(kubeConfigURL)") + + return try from(url: kubeConfigURL) + } + #endif + + #if os(Linux) || os(macOS) + public static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { + guard + let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], + let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] + else { + logger?.warning( + "Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set" + ) + return nil + } + + let apiServerUrl = + if host.contains(":") { + "https://[\(host)]:\(port)" + } else { + "https://\(host):\(port)" + } + + let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + guard FileManager.default.fileExists(atPath: tokenPath) else { + logger?.warning("Did not find service account token at \(tokenPath)") + return nil + } + + let namespaceFile = URL( + fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace") + let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) + if namespace == nil { + logger?.debug( + "Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace" + ) + } + + let certificateAuthorityFile = URL( + fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) + + return KubeConfig( + kind: "Config", + apiVersion: "v1", + clusters: [ + NamedCluster( + name: "kubernetes-cluster-local", + cluster: Cluster( + server: apiServerUrl, + insecureSkipTLSVerify: certificateAuthorityData == nil, + certificateAuthorityData: certificateAuthorityData + ) + ) + ], + users: [ + NamedAuthInfo( + name: "service-account-user", + authInfo: AuthInfo( + tokenFile: tokenPath + ) + ) + ], + contexts: [ + NamedContext( + name: "service-account-context", + context: Context( + cluster: "kubernetes-cluster-local", + user: "service-account-user", + namespace: namespace + ) + ) + ], + currentContext: "service-account-context" + ) + } + #endif } #if os(Linux) || os(macOS) -internal extension String { + extension String { - func stringByExpandingTildePath() -> String { - guard !isEmpty else { - return "" - } + func stringByExpandingTildePath() -> String { + guard !isEmpty else { + return "" + } - if self == "~" { - return FileManager.default.homeDirectoryForCurrentUser.path - } + if self == "~" { + return FileManager.default.homeDirectoryForCurrentUser.path + } - guard hasPrefix("~/") else { - return self - } + guard hasPrefix("~/") else { + return self + } - var relativePath = self - relativePath.removeFirst(2) + var relativePath = self + relativePath.removeFirst(2) - return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath - } -} + return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath + } + } #endif diff --git a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift index 189c1fb..580eff9 100644 --- a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift +++ b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift @@ -25,68 +25,127 @@ import Yams /// Configuration object for the ``KubernetesClient`` public struct KubernetesClientConfig: Sendable { - /// The URL for the kuberentes API server. - public let masterURL: URL - /// The namespace for the current client context. - public let namespace: String - /// The ``KubernetesClientAuthentication`` scheme. - public let authentication: KubernetesClientAuthentication - /// NIOSSL trust store sources fot the client. - public let trustRoots: NIOSSLTrustRoots? - /// Skips TLS verification for all API requests. - public let insecureSkipTLSVerify: Bool - /// The default timeout configuration for the underlying `HTTPClient`. - public let timeout: HTTPClient.Configuration.Timeout - /// The default redirect configuration for the underlying `HTTPCLient`. - public let redirectConfiguration: - HTTPClient.Configuration.RedirectConfiguration - /// URL to the proxy to be used for all requests made by this client. - public let proxyURL: URL? - /// Whether to request and decode gzipped responses from the API server. - public let gzip: Bool - - public init( - masterURL: URL, - namespace: String, - authentication: KubernetesClientAuthentication, - trustRoots: NIOSSLTrustRoots?, - insecureSkipTLSVerify: Bool, - timeout: HTTPClient.Configuration.Timeout, - redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration, - proxyURL: URL? = nil, - gzip: Bool = false - ) { - self.masterURL = masterURL - self.namespace = namespace - self.authentication = authentication - self.trustRoots = trustRoots - self.insecureSkipTLSVerify = insecureSkipTLSVerify - self.timeout = timeout - self.redirectConfiguration = redirectConfiguration - self.proxyURL = proxyURL - self.gzip = gzip - } + /// The URL for the kuberentes API server. + public let masterURL: URL + /// The namespace for the current client context. + public let namespace: String + /// The ``KubernetesClientAuthentication`` scheme. + public let authentication: KubernetesClientAuthentication + /// NIOSSL trust store sources fot the client. + public let trustRoots: NIOSSLTrustRoots? + /// Skips TLS verification for all API requests. + public let insecureSkipTLSVerify: Bool + /// The default timeout configuration for the underlying `HTTPClient`. + public let timeout: HTTPClient.Configuration.Timeout + /// The default redirect configuration for the underlying `HTTPCLient`. + public let redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration + /// URL to the proxy to be used for all requests made by this client. + public let proxyURL: URL? + /// Whether to request and decode gzipped responses from the API server. + public let gzip: Bool + + public init( + masterURL: URL, + namespace: String, + authentication: KubernetesClientAuthentication, + trustRoots: NIOSSLTrustRoots?, + insecureSkipTLSVerify: Bool, + timeout: HTTPClient.Configuration.Timeout, + redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration, + proxyURL: URL? = nil, + gzip: Bool = false + ) { + self.masterURL = masterURL + self.namespace = namespace + self.authentication = authentication + self.trustRoots = trustRoots + self.insecureSkipTLSVerify = insecureSkipTLSVerify + self.timeout = timeout + self.redirectConfiguration = redirectConfiguration + self.proxyURL = proxyURL + self.gzip = gzip + } +} + +// MARK: - CachedFileTokenSource + +/// A thread-safe, file-backed token source that caches the token with a synthetic TTL. +/// +/// The token file is re-read from disk only when the cached value has expired. +/// This mirrors the caching strategy used by client-go's `fileTokenSource` / +/// `cachingTokenSource`, which stamps each read with a synthetic 1-minute expiry +/// so the file is re-read roughly every minute rather than on every request. +public final class CachedFileTokenSource: @unchecked Sendable { + + /// The path to the token file on disk. + public var path: String { + lock.withLock { state.path } + } + + private struct State { + var path: String + var cachedToken: String? + var expiry: Date = .distantPast + var cacheDuration: TimeInterval + } + + private let lock = NSLock() + private var state: State + + /// Creates a new cached file token source. + /// - Parameters: + /// - path: The filesystem path to the token file. + /// - cacheDuration: How long to cache a token before re-reading from disk. Defaults to 60 seconds. + public init(path: String, cacheDuration: TimeInterval = 60) { + self.state = State(path: path, cacheDuration: cacheDuration) + } + + /// Returns the current token, re-reading from disk if the cache has expired. + public func token() -> String? { + lock.withLock { + let now = Date() + if let cachedToken = state.cachedToken, now < state.expiry { + return cachedToken + } + + guard let newToken = try? String(contentsOfFile: state.path, encoding: .utf8) else { + return nil + } + + let trimmed = newToken.trimmingCharacters(in: .whitespacesAndNewlines) + state.cachedToken = trimmed + state.expiry = now.addingTimeInterval(state.cacheDuration) + return trimmed + } + } } // MARK: - KubernetesClientAuthentication /// Supported client authentication schemes. public enum KubernetesClientAuthentication: Sendable { - /// Basic Authentincation via username/password. - case basicAuth(username: String, password: String) - /// Bearer token authentication scheme via a valid API token. - case bearer(token: String) - /// Certificate-based authenticaiton scheme with valid client certificate-key pair. - case x509(clientCertificate: NIOSSLCertificate, clientKey: NIOSSLPrivateKey) - - internal func authorizationHeader() -> String? { - switch self { - case let .basicAuth(username: username, password: password): - return HTTPClient.Authorization.basic(username: username, password: password).headerValue - case let .bearer(token: token): - return HTTPClient.Authorization.bearer(tokens: token).headerValue - default: - return nil - } - } + /// Basic Authentincation via username/password. + case basicAuth(username: String, password: String) + /// Bearer token authentication scheme via a valid API token. + case bearer(token: String) + /// File-backed bearer token with a cached token source that re-reads from disk periodically. + case tokenFile(source: CachedFileTokenSource) + /// Certificate-based authenticaiton scheme with valid client certificate-key pair. + case x509(clientCertificate: NIOSSLCertificate, clientKey: NIOSSLPrivateKey) + + internal func authorizationHeader() -> String? { + switch self { + case .basicAuth(let username, let password): + return HTTPClient.Authorization.basic(username: username, password: password).headerValue + case .bearer(let token): + return HTTPClient.Authorization.bearer(tokens: token).headerValue + case .tokenFile(let source): + guard let token = source.token() else { + return nil + } + return HTTPClient.Authorization.bearer(tokens: token).headerValue + default: + return nil + } + } } From 7dd7149746348bb7a36d086eb7fd0a8637dc7442 Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 19 Mar 2026 07:22:52 +0100 Subject: [PATCH 2/6] reformat code --- .../Config/AuthInfo+Authentication.swift | 238 +++++++-------- .../Config/KubeConfig+Loaders.swift | 271 +++++++++--------- .../Config/KubernetesClientConfig.swift | 210 +++++++------- 3 files changed, 361 insertions(+), 358 deletions(-) diff --git a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift index fd88f30..a599d5a 100644 --- a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift +++ b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift @@ -33,135 +33,135 @@ import Foundation import Logging import NIOSSL -extension AuthInfo { - - public func authentication(logger: Logger?) -> KubernetesClientAuthentication? { - if let username = username, let password = password { - return .basicAuth(username: username, password: password) - } - - if let token = token { - return .bearer(token: token) - } - - if let tokenFile = tokenFile { - return .tokenFile(source: CachedFileTokenSource(path: tokenFile)) - } - - do { - if let clientCertificateFile = clientCertificate, let clientKeyFile = clientKey { - let clientCertificate = try NIOSSLCertificate( - file: clientCertificateFile, - format: .pem - ) - let clientKey = try NIOSSLPrivateKey( - file: clientKeyFile, - format: .pem - ) - return .x509( - clientCertificate: clientCertificate, - clientKey: clientKey - ) - } - - if let clientCertificateData = clientCertificateData, let clientKeyData = clientKeyData { - let clientCertificate = try NIOSSLCertificate( - bytes: [UInt8](clientCertificateData), - format: .pem - ) - let clientKey = try NIOSSLPrivateKey( - bytes: [UInt8](clientKeyData), - format: .pem - ) - return .x509( - clientCertificate: clientCertificate, - clientKey: clientKey - ) - } - } catch { - logger?.warning( - "Error initializing authentication from client certificate: \(error)" - ) - } - - #if os(Linux) || os(macOS) - do { - if let exec { - let outputData = try run( - command: exec.command, - arguments: exec.args - ) - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - let credential = try decoder.decode( - ExecCredential.self, - from: outputData - ) - - return .bearer(token: credential.status.token) - } - } catch { - logger?.warning( - "Error initializing authentication from exec \(error)" - ) - } - #endif - - return nil - } +public extension AuthInfo { + + func authentication(logger: Logger?) -> KubernetesClientAuthentication? { + if let username = username, let password = password { + return .basicAuth(username: username, password: password) + } + + if let token = token { + return .bearer(token: token) + } + + if let tokenFile = tokenFile { + return .tokenFile(source: CachedFileTokenSource(path: tokenFile)) + } + + do { + if let clientCertificateFile = clientCertificate, let clientKeyFile = clientKey { + let clientCertificate = try NIOSSLCertificate( + file: clientCertificateFile, + format: .pem + ) + let clientKey = try NIOSSLPrivateKey( + file: clientKeyFile, + format: .pem + ) + return .x509( + clientCertificate: clientCertificate, + clientKey: clientKey + ) + } + + if let clientCertificateData = clientCertificateData, let clientKeyData = clientKeyData { + let clientCertificate = try NIOSSLCertificate( + bytes: [UInt8](clientCertificateData), + format: .pem + ) + let clientKey = try NIOSSLPrivateKey( + bytes: [UInt8](clientKeyData), + format: .pem + ) + return .x509( + clientCertificate: clientCertificate, + clientKey: clientKey + ) + } + } catch { + logger?.warning( + "Error initializing authentication from client certificate: \(error)" + ) + } + + #if os(Linux) || os(macOS) + do { + if let exec { + let outputData = try run( + command: exec.command, + arguments: exec.args + ) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let credential = try decoder.decode( + ExecCredential.self, + from: outputData + ) + + return .bearer(token: credential.status.token) + } + } catch { + logger?.warning( + "Error initializing authentication from exec \(error)" + ) + } + #endif + + return nil + } } // MARK: - ExecCredential -// It seems that AWS doesn't implement properly the model for client.authentication.k8s.io/v1beta1 -// Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ -// ExecCredential.Spec.interactive is required as long as the ones in the Status object. +/// It seems that AWS doesn't implement properly the model for client.authentication.k8s.io/v1beta1 +/// Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ +/// ExecCredential.Spec.interactive is required as long as the ones in the Status object. public struct ExecCredential: Codable { - let apiVersion: String - let kind: String - let spec: Spec - let status: Status + let apiVersion: String + let kind: String + let spec: Spec + let status: Status } -extension ExecCredential { +public extension ExecCredential { - public struct Spec: Codable { - let cluster: Cluster? - let interactive: Bool? - } + struct Spec: Codable { + let cluster: Cluster? + let interactive: Bool? + } - public struct Status: Codable { - let expirationTimestamp: Date - let token: String - let clientCertificateData: String? - let clientKeyData: String? - } + struct Status: Codable { + let expirationTimestamp: Date + let token: String + let clientCertificateData: String? + let clientKeyData: String? + } } #if os(Linux) || os(macOS) - internal func run(command: String, arguments: [String]? = nil) throws -> Data { - func run(_ command: String, _ arguments: [String]?) throws -> Data { - let task = Process() - task.executableURL = URL(fileURLWithPath: command) - arguments.flatMap { task.arguments = $0 } - - let pipe = Pipe() - task.standardOutput = pipe - - try task.run() - - return pipe.fileHandleForReading.availableData - } - - func resolve(command: String) throws -> String { - try String( - decoding: - run("/usr/bin/which", ["\(command)"]), - as: UTF8.self - ).trimmingCharacters(in: .whitespacesAndNewlines) - } - - return try run(resolve(command: command), arguments) - } + internal func run(command: String, arguments: [String]? = nil) throws -> Data { + func run(_ command: String, _ arguments: [String]?) throws -> Data { + let task = Process() + task.executableURL = URL(fileURLWithPath: command) + arguments.flatMap { task.arguments = $0 } + + let pipe = Pipe() + task.standardOutput = pipe + + try task.run() + + return pipe.fileHandleForReading.availableData + } + + func resolve(command: String) throws -> String { + try String( + decoding: + run("/usr/bin/which", ["\(command)"]), + as: UTF8.self + ).trimmingCharacters(in: .whitespacesAndNewlines) + } + + return try run(resolve(command: command), arguments) + } #endif diff --git a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift index 8e6495e..563af64 100644 --- a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift +++ b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift @@ -18,146 +18,149 @@ import Foundation import Logging import Yams -extension KubeConfig { - - public static func from(config: String) throws -> KubeConfig { - let decoder = YAMLDecoder() - return try decoder.decode(KubeConfig.self, from: config) - } - - public static func from(url: URL) throws -> KubeConfig { - let contents = try String(contentsOf: url, encoding: .utf8) - - return try from(config: contents) - } - - #if os(Linux) || os(macOS) - public static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws - -> KubeConfig? - { - guard let varContent = ProcessInfo.processInfo.environment[envVar] else { - logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") - return nil - } - - let expanded = varContent.stringByExpandingTildePath() - let kubeConfigURL = URL(fileURLWithPath: expanded) - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) - } - #endif - - #if os(Linux) || os(macOS) - public static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { - guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { - logger?.info( - "Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set.") - return nil - } - - let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) - } - #endif - - #if os(Linux) || os(macOS) - public static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { - guard - let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], - let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] - else { - logger?.warning( - "Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set" - ) - return nil - } - - let apiServerUrl = - if host.contains(":") { - "https://[\(host)]:\(port)" - } else { - "https://\(host):\(port)" - } - - let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" - guard FileManager.default.fileExists(atPath: tokenPath) else { - logger?.warning("Did not find service account token at \(tokenPath)") - return nil - } - - let namespaceFile = URL( - fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace") - let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) - if namespace == nil { - logger?.debug( - "Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace" - ) - } - - let certificateAuthorityFile = URL( - fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") - let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) - - return KubeConfig( - kind: "Config", - apiVersion: "v1", - clusters: [ - NamedCluster( - name: "kubernetes-cluster-local", - cluster: Cluster( - server: apiServerUrl, - insecureSkipTLSVerify: certificateAuthorityData == nil, - certificateAuthorityData: certificateAuthorityData - ) - ) - ], - users: [ - NamedAuthInfo( - name: "service-account-user", - authInfo: AuthInfo( - tokenFile: tokenPath - ) - ) - ], - contexts: [ - NamedContext( - name: "service-account-context", - context: Context( - cluster: "kubernetes-cluster-local", - user: "service-account-user", - namespace: namespace - ) - ) - ], - currentContext: "service-account-context" - ) - } - #endif +public extension KubeConfig { + + static func from(config: String) throws -> KubeConfig { + let decoder = YAMLDecoder() + return try decoder.decode(KubeConfig.self, from: config) + } + + static func from(url: URL) throws -> KubeConfig { + let contents = try String(contentsOf: url, encoding: .utf8) + + return try from(config: contents) + } + + #if os(Linux) || os(macOS) + static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws + -> KubeConfig? + { + guard let varContent = ProcessInfo.processInfo.environment[envVar] else { + logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") + return nil + } + + let expanded = varContent.stringByExpandingTildePath() + let kubeConfigURL = URL(fileURLWithPath: expanded) + logger?.info("Loading configuration from \(kubeConfigURL)") + + return try from(url: kubeConfigURL) + } + #endif + + #if os(Linux) || os(macOS) + static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { + guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { + logger?.info( + "Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set." + ) + return nil + } + + let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") + logger?.info("Loading configuration from \(kubeConfigURL)") + + return try from(url: kubeConfigURL) + } + #endif + + #if os(Linux) || os(macOS) + static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { + guard + let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], + let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] + else { + logger?.warning( + "Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set" + ) + return nil + } + + let apiServerUrl = + if host.contains(":") { + "https://[\(host)]:\(port)" + } else { + "https://\(host):\(port)" + } + + let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + guard FileManager.default.fileExists(atPath: tokenPath) else { + logger?.warning("Did not find service account token at \(tokenPath)") + return nil + } + + let namespaceFile = URL( + fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + ) + let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) + if namespace == nil { + logger?.debug( + "Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace" + ) + } + + let certificateAuthorityFile = URL( + fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + ) + let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) + + return KubeConfig( + kind: "Config", + apiVersion: "v1", + clusters: [ + NamedCluster( + name: "kubernetes-cluster-local", + cluster: Cluster( + server: apiServerUrl, + insecureSkipTLSVerify: certificateAuthorityData == nil, + certificateAuthorityData: certificateAuthorityData + ) + ), + ], + users: [ + NamedAuthInfo( + name: "service-account-user", + authInfo: AuthInfo( + tokenFile: tokenPath + ) + ), + ], + contexts: [ + NamedContext( + name: "service-account-context", + context: Context( + cluster: "kubernetes-cluster-local", + user: "service-account-user", + namespace: namespace + ) + ), + ], + currentContext: "service-account-context" + ) + } + #endif } #if os(Linux) || os(macOS) - extension String { + extension String { - func stringByExpandingTildePath() -> String { - guard !isEmpty else { - return "" - } + func stringByExpandingTildePath() -> String { + guard !isEmpty else { + return "" + } - if self == "~" { - return FileManager.default.homeDirectoryForCurrentUser.path - } + if self == "~" { + return FileManager.default.homeDirectoryForCurrentUser.path + } - guard hasPrefix("~/") else { - return self - } + guard hasPrefix("~/") else { + return self + } - var relativePath = self - relativePath.removeFirst(2) + var relativePath = self + relativePath.removeFirst(2) - return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath - } - } + return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath + } + } #endif diff --git a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift index 580eff9..d018ffd 100644 --- a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift +++ b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift @@ -25,46 +25,46 @@ import Yams /// Configuration object for the ``KubernetesClient`` public struct KubernetesClientConfig: Sendable { - /// The URL for the kuberentes API server. - public let masterURL: URL - /// The namespace for the current client context. - public let namespace: String - /// The ``KubernetesClientAuthentication`` scheme. - public let authentication: KubernetesClientAuthentication - /// NIOSSL trust store sources fot the client. - public let trustRoots: NIOSSLTrustRoots? - /// Skips TLS verification for all API requests. - public let insecureSkipTLSVerify: Bool - /// The default timeout configuration for the underlying `HTTPClient`. - public let timeout: HTTPClient.Configuration.Timeout - /// The default redirect configuration for the underlying `HTTPCLient`. - public let redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration - /// URL to the proxy to be used for all requests made by this client. - public let proxyURL: URL? - /// Whether to request and decode gzipped responses from the API server. - public let gzip: Bool - - public init( - masterURL: URL, - namespace: String, - authentication: KubernetesClientAuthentication, - trustRoots: NIOSSLTrustRoots?, - insecureSkipTLSVerify: Bool, - timeout: HTTPClient.Configuration.Timeout, - redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration, - proxyURL: URL? = nil, - gzip: Bool = false - ) { - self.masterURL = masterURL - self.namespace = namespace - self.authentication = authentication - self.trustRoots = trustRoots - self.insecureSkipTLSVerify = insecureSkipTLSVerify - self.timeout = timeout - self.redirectConfiguration = redirectConfiguration - self.proxyURL = proxyURL - self.gzip = gzip - } + /// The URL for the kuberentes API server. + public let masterURL: URL + /// The namespace for the current client context. + public let namespace: String + /// The ``KubernetesClientAuthentication`` scheme. + public let authentication: KubernetesClientAuthentication + /// NIOSSL trust store sources fot the client. + public let trustRoots: NIOSSLTrustRoots? + /// Skips TLS verification for all API requests. + public let insecureSkipTLSVerify: Bool + /// The default timeout configuration for the underlying `HTTPClient`. + public let timeout: HTTPClient.Configuration.Timeout + /// The default redirect configuration for the underlying `HTTPCLient`. + public let redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration + /// URL to the proxy to be used for all requests made by this client. + public let proxyURL: URL? + /// Whether to request and decode gzipped responses from the API server. + public let gzip: Bool + + public init( + masterURL: URL, + namespace: String, + authentication: KubernetesClientAuthentication, + trustRoots: NIOSSLTrustRoots?, + insecureSkipTLSVerify: Bool, + timeout: HTTPClient.Configuration.Timeout, + redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration, + proxyURL: URL? = nil, + gzip: Bool = false + ) { + self.masterURL = masterURL + self.namespace = namespace + self.authentication = authentication + self.trustRoots = trustRoots + self.insecureSkipTLSVerify = insecureSkipTLSVerify + self.timeout = timeout + self.redirectConfiguration = redirectConfiguration + self.proxyURL = proxyURL + self.gzip = gzip + } } // MARK: - CachedFileTokenSource @@ -77,75 +77,75 @@ public struct KubernetesClientConfig: Sendable { /// so the file is re-read roughly every minute rather than on every request. public final class CachedFileTokenSource: @unchecked Sendable { - /// The path to the token file on disk. - public var path: String { - lock.withLock { state.path } - } - - private struct State { - var path: String - var cachedToken: String? - var expiry: Date = .distantPast - var cacheDuration: TimeInterval - } - - private let lock = NSLock() - private var state: State - - /// Creates a new cached file token source. - /// - Parameters: - /// - path: The filesystem path to the token file. - /// - cacheDuration: How long to cache a token before re-reading from disk. Defaults to 60 seconds. - public init(path: String, cacheDuration: TimeInterval = 60) { - self.state = State(path: path, cacheDuration: cacheDuration) - } - - /// Returns the current token, re-reading from disk if the cache has expired. - public func token() -> String? { - lock.withLock { - let now = Date() - if let cachedToken = state.cachedToken, now < state.expiry { - return cachedToken - } - - guard let newToken = try? String(contentsOfFile: state.path, encoding: .utf8) else { - return nil - } - - let trimmed = newToken.trimmingCharacters(in: .whitespacesAndNewlines) - state.cachedToken = trimmed - state.expiry = now.addingTimeInterval(state.cacheDuration) - return trimmed - } - } + /// The path to the token file on disk. + public var path: String { + lock.withLock { state.path } + } + + private struct State { + var path: String + var cachedToken: String? + var expiry: Date = .distantPast + var cacheDuration: TimeInterval + } + + private let lock = NSLock() + private var state: State + + /// Creates a new cached file token source. + /// - Parameters: + /// - path: The filesystem path to the token file. + /// - cacheDuration: How long to cache a token before re-reading from disk. Defaults to 60 seconds. + public init(path: String, cacheDuration: TimeInterval = 60) { + self.state = State(path: path, cacheDuration: cacheDuration) + } + + /// Returns the current token, re-reading from disk if the cache has expired. + public func token() -> String? { + lock.withLock { + let now = Date() + if let cachedToken = state.cachedToken, now < state.expiry { + return cachedToken + } + + guard let newToken = try? String(contentsOfFile: state.path, encoding: .utf8) else { + return nil + } + + let trimmed = newToken.trimmingCharacters(in: .whitespacesAndNewlines) + state.cachedToken = trimmed + state.expiry = now.addingTimeInterval(state.cacheDuration) + return trimmed + } + } } // MARK: - KubernetesClientAuthentication /// Supported client authentication schemes. public enum KubernetesClientAuthentication: Sendable { - /// Basic Authentincation via username/password. - case basicAuth(username: String, password: String) - /// Bearer token authentication scheme via a valid API token. - case bearer(token: String) - /// File-backed bearer token with a cached token source that re-reads from disk periodically. - case tokenFile(source: CachedFileTokenSource) - /// Certificate-based authenticaiton scheme with valid client certificate-key pair. - case x509(clientCertificate: NIOSSLCertificate, clientKey: NIOSSLPrivateKey) - - internal func authorizationHeader() -> String? { - switch self { - case .basicAuth(let username, let password): - return HTTPClient.Authorization.basic(username: username, password: password).headerValue - case .bearer(let token): - return HTTPClient.Authorization.bearer(tokens: token).headerValue - case .tokenFile(let source): - guard let token = source.token() else { - return nil - } - return HTTPClient.Authorization.bearer(tokens: token).headerValue - default: - return nil - } - } + /// Basic Authentincation via username/password. + case basicAuth(username: String, password: String) + /// Bearer token authentication scheme via a valid API token. + case bearer(token: String) + /// File-backed bearer token with a cached token source that re-reads from disk periodically. + case tokenFile(source: CachedFileTokenSource) + /// Certificate-based authenticaiton scheme with valid client certificate-key pair. + case x509(clientCertificate: NIOSSLCertificate, clientKey: NIOSSLPrivateKey) + + internal func authorizationHeader() -> String? { + switch self { + case let .basicAuth(username, password): + return HTTPClient.Authorization.basic(username: username, password: password).headerValue + case let .bearer(token): + return HTTPClient.Authorization.bearer(tokens: token).headerValue + case let .tokenFile(source): + guard let token = source.token() else { + return nil + } + return HTTPClient.Authorization.bearer(tokens: token).headerValue + default: + return nil + } + } } From 928508e0f762b657cebe8b4bcc1819e9e1a0209c Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 19 Mar 2026 07:30:39 +0100 Subject: [PATCH 3/6] use NIODeadline for the TTL of the token --- .../Config/KubernetesClientConfig.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift index d018ffd..4dda65d 100644 --- a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift +++ b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift @@ -19,6 +19,7 @@ import Foundation import Logging import NIOSSL import Yams +import NIOCore // MARK: - KubernetesClientConfig @@ -85,8 +86,8 @@ public final class CachedFileTokenSource: @unchecked Sendable { private struct State { var path: String var cachedToken: String? - var expiry: Date = .distantPast - var cacheDuration: TimeInterval + var expiry: NIODeadline = .distantPast + var cacheDuration: TimeAmount } private let lock = NSLock() @@ -96,14 +97,14 @@ public final class CachedFileTokenSource: @unchecked Sendable { /// - Parameters: /// - path: The filesystem path to the token file. /// - cacheDuration: How long to cache a token before re-reading from disk. Defaults to 60 seconds. - public init(path: String, cacheDuration: TimeInterval = 60) { + public init(path: String, cacheDuration: TimeAmount = .seconds(60)) { self.state = State(path: path, cacheDuration: cacheDuration) } /// Returns the current token, re-reading from disk if the cache has expired. public func token() -> String? { lock.withLock { - let now = Date() + let now = NIODeadline.now() if let cachedToken = state.cachedToken, now < state.expiry { return cachedToken } @@ -114,7 +115,7 @@ public final class CachedFileTokenSource: @unchecked Sendable { let trimmed = newToken.trimmingCharacters(in: .whitespacesAndNewlines) state.cachedToken = trimmed - state.expiry = now.addingTimeInterval(state.cacheDuration) + state.expiry = now + state.cacheDuration return trimmed } } From dbedc123c6afd96cbaef5b3c2fe31f31dceeb8ea Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 19 Mar 2026 07:40:07 +0100 Subject: [PATCH 4/6] revert cosmetic changes --- .../Config/AuthInfo+Authentication.swift | 6 +- .../Config/KubeConfig+Loaders.swift | 211 ++++++++---------- .../Config/KubernetesClientConfig.swift | 11 +- 3 files changed, 108 insertions(+), 120 deletions(-) diff --git a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift index a599d5a..747f764 100644 --- a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift +++ b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift @@ -114,9 +114,9 @@ public extension AuthInfo { // MARK: - ExecCredential -/// It seems that AWS doesn't implement properly the model for client.authentication.k8s.io/v1beta1 -/// Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ -/// ExecCredential.Spec.interactive is required as long as the ones in the Status object. +// It seems that AWS doesn't implement properly the model for client.authentication.k8s.io/v1beta1 +// Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ +// ExecCredential.Spec.interactive is required as long as the ones in the Status object. public struct ExecCredential: Codable { let apiVersion: String let kind: String diff --git a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift index 563af64..4cca296 100644 --- a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift +++ b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift @@ -32,135 +32,122 @@ public extension KubeConfig { } #if os(Linux) || os(macOS) - static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws - -> KubeConfig? - { - guard let varContent = ProcessInfo.processInfo.environment[envVar] else { - logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") - return nil - } - - let expanded = varContent.stringByExpandingTildePath() - let kubeConfigURL = URL(fileURLWithPath: expanded) - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) + static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws -> KubeConfig? { + guard let varContent = ProcessInfo.processInfo.environment[envVar] else { + logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") + return nil } + + let expanded = varContent.stringByExpandingTildePath() + let kubeConfigURL = URL(fileURLWithPath: expanded) + logger?.info("Loading configuration from \(kubeConfigURL)") + + return try from(url: kubeConfigURL) + } #endif #if os(Linux) || os(macOS) - static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { - guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { - logger?.info( - "Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set." - ) - return nil - } - - let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) + static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { + guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { + logger?.info("Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set.") + return nil } + + let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") + logger?.info("Loading configuration from \(kubeConfigURL)") + + return try from(url: kubeConfigURL) + } #endif #if os(Linux) || os(macOS) - static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { - guard - let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], - let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] - else { - logger?.warning( - "Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set" - ) - return nil - } - - let apiServerUrl = - if host.contains(":") { - "https://[\(host)]:\(port)" - } else { - "https://\(host):\(port)" - } - - let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" - guard FileManager.default.fileExists(atPath: tokenPath) else { - logger?.warning("Did not find service account token at \(tokenPath)") - return nil - } - - let namespaceFile = URL( - fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace" - ) - let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) - if namespace == nil { - logger?.debug( - "Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace" - ) - } - - let certificateAuthorityFile = URL( - fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - ) - let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) - - return KubeConfig( - kind: "Config", - apiVersion: "v1", - clusters: [ - NamedCluster( - name: "kubernetes-cluster-local", - cluster: Cluster( - server: apiServerUrl, - insecureSkipTLSVerify: certificateAuthorityData == nil, - certificateAuthorityData: certificateAuthorityData - ) - ), - ], - users: [ - NamedAuthInfo( - name: "service-account-user", - authInfo: AuthInfo( - tokenFile: tokenPath - ) - ), - ], - contexts: [ - NamedContext( - name: "service-account-context", - context: Context( - cluster: "kubernetes-cluster-local", - user: "service-account-user", - namespace: namespace - ) - ), - ], - currentContext: "service-account-context" - ) + static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { + guard + let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], + let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] + else { + logger?.warning("Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set") + return nil + } + + let apiServerUrl = if host.contains(":") { + "https://[\(host)]:\(port)" + } else { + "https://\(host):\(port)" + } + + let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + guard FileManager.default.fileExists(atPath: tokenPath) else { + logger?.warning("Did not find service account token at \(tokenPath)") + return nil } + + let namespaceFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace") + let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) + if namespace == nil { + logger?.debug("Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace") + } + + let certificateAuthorityFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) + + return KubeConfig( + kind: "Config", + apiVersion: "v1", + clusters: [ + NamedCluster( + name: "kubernetes-cluster-local", + cluster: Cluster( + server: apiServerUrl, + insecureSkipTLSVerify: certificateAuthorityData == nil, + certificateAuthorityData: certificateAuthorityData + ) + ), + ], + users: [ + NamedAuthInfo( + name: "service-account-user", + authInfo: AuthInfo( + tokenFile: tokenPath + ) + ), + ], + contexts: [ + NamedContext( + name: "service-account-context", + context: Context( + cluster: "kubernetes-cluster-local", + user: "service-account-user", + namespace: namespace + ) + ), + ], + currentContext: "service-account-context" + ) + } #endif } #if os(Linux) || os(macOS) - extension String { +internal extension String { - func stringByExpandingTildePath() -> String { - guard !isEmpty else { - return "" - } + func stringByExpandingTildePath() -> String { + guard !isEmpty else { + return "" + } - if self == "~" { - return FileManager.default.homeDirectoryForCurrentUser.path - } + if self == "~" { + return FileManager.default.homeDirectoryForCurrentUser.path + } - guard hasPrefix("~/") else { - return self - } + guard hasPrefix("~/") else { + return self + } - var relativePath = self - relativePath.removeFirst(2) + var relativePath = self + relativePath.removeFirst(2) - return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath - } + return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath } +} #endif diff --git a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift index 4dda65d..3d3712e 100644 --- a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift +++ b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift @@ -17,9 +17,9 @@ import AsyncHTTPClient import Foundation import Logging +import NIOCore import NIOSSL import Yams -import NIOCore // MARK: - KubernetesClientConfig @@ -39,7 +39,8 @@ public struct KubernetesClientConfig: Sendable { /// The default timeout configuration for the underlying `HTTPClient`. public let timeout: HTTPClient.Configuration.Timeout /// The default redirect configuration for the underlying `HTTPCLient`. - public let redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration + public let redirectConfiguration: + HTTPClient.Configuration.RedirectConfiguration /// URL to the proxy to be used for all requests made by this client. public let proxyURL: URL? /// Whether to request and decode gzipped responses from the API server. @@ -136,11 +137,11 @@ public enum KubernetesClientAuthentication: Sendable { internal func authorizationHeader() -> String? { switch self { - case let .basicAuth(username, password): + case let .basicAuth(username: username, password: password): return HTTPClient.Authorization.basic(username: username, password: password).headerValue - case let .bearer(token): + case let .bearer(token: token): return HTTPClient.Authorization.bearer(tokens: token).headerValue - case let .tokenFile(source): + case let .tokenFile(source: source): guard let token = source.token() else { return nil } From 36254b42dc75e41e85322ed657a3c2cb27a8dd93 Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 19 Mar 2026 10:51:58 +0100 Subject: [PATCH 5/6] fix linter issues --- .../Client/KubernetesClient.swift | 54 ++--- .../Client/KubernetesRequest.swift | 2 +- .../Config/AuthInfo+Authentication.swift | 6 +- .../Config/KubeConfig+Loaders.swift | 194 +++++++++--------- .../Config/KubernetesClientConfig+Init.swift | 88 ++++---- .../KubernetesClient+Discovery.swift | 4 +- 6 files changed, 173 insertions(+), 175 deletions(-) diff --git a/Sources/SwiftkubeClient/Client/KubernetesClient.swift b/Sources/SwiftkubeClient/Client/KubernetesClient.swift index affcf8f..061943b 100644 --- a/Sources/SwiftkubeClient/Client/KubernetesClient.swift +++ b/Sources/SwiftkubeClient/Client/KubernetesClient.swift @@ -105,34 +105,34 @@ public actor KubernetesClient { #endif #if os(Linux) || os(macOS) - /// Create a new instance of the Kubernetes client. - /// - /// The client tries to resolve a `kube config` automatically from different sources in the following order: - /// - /// - A Kube config file at path of environment variable `KUBECONFIG` (if set) - /// - A Kube config file in the user's `$HOME/.kube/config` directory - /// - `ServiceAccount` token located at `/var/run/secrets/kubernetes.io/serviceaccount/token` and a mounted CA certificate, if it's running in Kubernetes. - /// - /// Returns `nil` if a configuration can't be found. - /// - /// - Note: This initializer is only available on Linux and macOS. On iOS, tvOS, and watchOS, you must use - /// `init(config:provider:logger:)` with a manually configured `KubernetesClientConfig`. - /// - /// - Parameters: - /// - provider: A ``EventLoopGroupProvider`` to specify how ``EventLoopGroup`` will be created. - /// - logger: The logger to use for this client. - public init?( - provider: HTTPClient.EventLoopGroupProvider = .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1)), - logger: Logger? = nil - ) { - guard - let config = try? KubernetesClientConfig.initialize(logger: logger) - else { - return nil - } + /// Create a new instance of the Kubernetes client. + /// + /// The client tries to resolve a `kube config` automatically from different sources in the following order: + /// + /// - A Kube config file at path of environment variable `KUBECONFIG` (if set) + /// - A Kube config file in the user's `$HOME/.kube/config` directory + /// - `ServiceAccount` token located at `/var/run/secrets/kubernetes.io/serviceaccount/token` and a mounted CA certificate, if it's running in Kubernetes. + /// + /// Returns `nil` if a configuration can't be found. + /// + /// - Note: This initializer is only available on Linux and macOS. On iOS, tvOS, and watchOS, you must use + /// `init(config:provider:logger:)` with a manually configured `KubernetesClientConfig`. + /// + /// - Parameters: + /// - provider: A ``EventLoopGroupProvider`` to specify how ``EventLoopGroup`` will be created. + /// - logger: The logger to use for this client. + public init?( + provider: HTTPClient.EventLoopGroupProvider = .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1)), + logger: Logger? = nil + ) { + guard + let config = try? KubernetesClientConfig.initialize(logger: logger) + else { + return nil + } - self.init(config: config, provider: provider, logger: logger) - } + self.init(config: config, provider: provider, logger: logger) + } #endif /// Create a new instance of the Kubernetes client. diff --git a/Sources/SwiftkubeClient/Client/KubernetesRequest.swift b/Sources/SwiftkubeClient/Client/KubernetesRequest.swift index eb8557a..517410b 100644 --- a/Sources/SwiftkubeClient/Client/KubernetesRequest.swift +++ b/Sources/SwiftkubeClient/Client/KubernetesRequest.swift @@ -102,7 +102,7 @@ public struct KubernetesRequest: Sendable { // MARK: - RequestBody -internal enum RequestBody: Sendable { +internal enum RequestBody { case resource(payload: any KubernetesAPIResource) case subResource(type: ResourceType, payload: any KubernetesResource) diff --git a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift index 747f764..a599d5a 100644 --- a/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift +++ b/Sources/SwiftkubeClient/Config/AuthInfo+Authentication.swift @@ -114,9 +114,9 @@ public extension AuthInfo { // MARK: - ExecCredential -// It seems that AWS doesn't implement properly the model for client.authentication.k8s.io/v1beta1 -// Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ -// ExecCredential.Spec.interactive is required as long as the ones in the Status object. +/// It seems that AWS doesn't implement properly the model for client.authentication.k8s.io/v1beta1 +/// Acordingly with the doc https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ +/// ExecCredential.Spec.interactive is required as long as the ones in the Status object. public struct ExecCredential: Codable { let apiVersion: String let kind: String diff --git a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift index 4cca296..a7e64a6 100644 --- a/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift +++ b/Sources/SwiftkubeClient/Config/KubeConfig+Loaders.swift @@ -32,122 +32,122 @@ public extension KubeConfig { } #if os(Linux) || os(macOS) - static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws -> KubeConfig? { - guard let varContent = ProcessInfo.processInfo.environment[envVar] else { - logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") - return nil - } + static func fromEnvironment(envVar: String = "KUBECONFIG", logger: Logger? = nil) throws -> KubeConfig? { + guard let varContent = ProcessInfo.processInfo.environment[envVar] else { + logger?.info("Skipping kubeconfig because environment variable \(envVar) is not set") + return nil + } - let expanded = varContent.stringByExpandingTildePath() - let kubeConfigURL = URL(fileURLWithPath: expanded) - logger?.info("Loading configuration from \(kubeConfigURL)") + let expanded = varContent.stringByExpandingTildePath() + let kubeConfigURL = URL(fileURLWithPath: expanded) + logger?.info("Loading configuration from \(kubeConfigURL)") - return try from(url: kubeConfigURL) - } - #endif - - #if os(Linux) || os(macOS) - static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { - guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { - logger?.info("Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set.") - return nil + return try from(url: kubeConfigURL) } - - let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") - logger?.info("Loading configuration from \(kubeConfigURL)") - - return try from(url: kubeConfigURL) - } #endif #if os(Linux) || os(macOS) - static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { - guard - let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], - let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] - else { - logger?.warning("Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set") - return nil - } + static func fromDefaultLocalConfig(logger: Logger? = nil) throws -> KubeConfig? { + guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { + logger?.info("Skipping kubeconfig in $HOME/.kube/config because HOME env variable is not set.") + return nil + } - let apiServerUrl = if host.contains(":") { - "https://[\(host)]:\(port)" - } else { - "https://\(host):\(port)" - } + let kubeConfigURL = URL(fileURLWithPath: homePath + "/.kube/config") + logger?.info("Loading configuration from \(kubeConfigURL)") - let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" - guard FileManager.default.fileExists(atPath: tokenPath) else { - logger?.warning("Did not find service account token at \(tokenPath)") - return nil + return try from(url: kubeConfigURL) } + #endif - let namespaceFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace") - let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) - if namespace == nil { - logger?.debug("Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace") + #if os(Linux) || os(macOS) + static func fromServiceAccount(logger: Logger? = nil) throws -> KubeConfig? { + guard + let host = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_HOST"], + let port = ProcessInfo.processInfo.environment["KUBERNETES_SERVICE_PORT"] + else { + logger?.warning("Skipping service account kubeconfig because either KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT is not set") + return nil + } + + let apiServerUrl = if host.contains(":") { + "https://[\(host)]:\(port)" + } else { + "https://\(host):\(port)" + } + + let tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + guard FileManager.default.fileExists(atPath: tokenPath) else { + logger?.warning("Did not find service account token at \(tokenPath)") + return nil + } + + let namespaceFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespace") + let namespace = try? String(contentsOf: namespaceFile, encoding: .utf8) + if namespace == nil { + logger?.debug("Did not find service account namespace at /var/run/secrets/kubernetes.io/serviceaccount/namespace") + } + + let certificateAuthorityFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) + + return KubeConfig( + kind: "Config", + apiVersion: "v1", + clusters: [ + NamedCluster( + name: "kubernetes-cluster-local", + cluster: Cluster( + server: apiServerUrl, + insecureSkipTLSVerify: certificateAuthorityData == nil, + certificateAuthorityData: certificateAuthorityData + ) + ), + ], + users: [ + NamedAuthInfo( + name: "service-account-user", + authInfo: AuthInfo( + tokenFile: tokenPath + ) + ), + ], + contexts: [ + NamedContext( + name: "service-account-context", + context: Context( + cluster: "kubernetes-cluster-local", + user: "service-account-user", + namespace: namespace + ) + ), + ], + currentContext: "service-account-context" + ) } - - let certificateAuthorityFile = URL(fileURLWithPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") - let certificateAuthorityData = try? Data(contentsOf: certificateAuthorityFile) - - return KubeConfig( - kind: "Config", - apiVersion: "v1", - clusters: [ - NamedCluster( - name: "kubernetes-cluster-local", - cluster: Cluster( - server: apiServerUrl, - insecureSkipTLSVerify: certificateAuthorityData == nil, - certificateAuthorityData: certificateAuthorityData - ) - ), - ], - users: [ - NamedAuthInfo( - name: "service-account-user", - authInfo: AuthInfo( - tokenFile: tokenPath - ) - ), - ], - contexts: [ - NamedContext( - name: "service-account-context", - context: Context( - cluster: "kubernetes-cluster-local", - user: "service-account-user", - namespace: namespace - ) - ), - ], - currentContext: "service-account-context" - ) - } #endif } #if os(Linux) || os(macOS) -internal extension String { + internal extension String { - func stringByExpandingTildePath() -> String { - guard !isEmpty else { - return "" - } + func stringByExpandingTildePath() -> String { + guard !isEmpty else { + return "" + } - if self == "~" { - return FileManager.default.homeDirectoryForCurrentUser.path - } + if self == "~" { + return FileManager.default.homeDirectoryForCurrentUser.path + } - guard hasPrefix("~/") else { - return self - } + guard hasPrefix("~/") else { + return self + } - var relativePath = self - relativePath.removeFirst(2) + var relativePath = self + relativePath.removeFirst(2) - return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath + return FileManager.default.homeDirectoryForCurrentUser.path + "/" + relativePath + } } -} #endif diff --git a/Sources/SwiftkubeClient/Config/KubernetesClientConfig+Init.swift b/Sources/SwiftkubeClient/Config/KubernetesClientConfig+Init.swift index 0028f0c..0921d18 100644 --- a/Sources/SwiftkubeClient/Config/KubernetesClientConfig+Init.swift +++ b/Sources/SwiftkubeClient/Config/KubernetesClientConfig+Init.swift @@ -23,53 +23,53 @@ import Yams public extension KubernetesClientConfig { #if os(Linux) || os(macOS) - /// Initializes a client configuration. - /// - /// This factory method tries to resolve a `kube config` automatically from different sources in the following order: - /// - /// - A Kube config file at path of environment variable `KUBECONFIG` (if set) - /// - A Kube config file in the user's `$HOME/.kube/config` directory - /// - `ServiceAccount` token located at `/var/run/secrets/kubernetes.io/serviceaccount/token` and a mounted CA certificate, if it's running in Kubernetes. - /// - /// It is also possible to override the default values for the underlying `HTTPClient` timeout and redirect config. - /// - /// - Note: This method is only available on Linux and macOS. On iOS, tvOS, and watchOS, you must manually configure - /// the client using `KubernetesClientConfig.init()` or `from(kubeConfig:)`. - /// - /// - Parameters: - /// - timeout: The desired timeout configuration to apply. If not provided, then `connect` timeout will default to 10 seconds. - /// - redirectConfiguration: Specifies redirect processing settings. If not provided, then it will default to a maximum of 5 follows w/o cycles. - /// - logger: The logger to use for the underlying configuration loaders. - /// - Returns: An instance of KubernetesClientConfig for the Swiftkube KubernetesClient - static func initialize( - timeout: HTTPClient.Configuration.Timeout? = nil, - redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration? = nil, - logger: Logger? - ) throws -> KubernetesClientConfig? { - let kubeConfig: KubeConfig? = { - if let config = try? KubeConfig.fromEnvironment() { - return config - } - - if let config = try? KubeConfig.fromDefaultLocalConfig() { - return config + /// Initializes a client configuration. + /// + /// This factory method tries to resolve a `kube config` automatically from different sources in the following order: + /// + /// - A Kube config file at path of environment variable `KUBECONFIG` (if set) + /// - A Kube config file in the user's `$HOME/.kube/config` directory + /// - `ServiceAccount` token located at `/var/run/secrets/kubernetes.io/serviceaccount/token` and a mounted CA certificate, if it's running in Kubernetes. + /// + /// It is also possible to override the default values for the underlying `HTTPClient` timeout and redirect config. + /// + /// - Note: This method is only available on Linux and macOS. On iOS, tvOS, and watchOS, you must manually configure + /// the client using `KubernetesClientConfig.init()` or `from(kubeConfig:)`. + /// + /// - Parameters: + /// - timeout: The desired timeout configuration to apply. If not provided, then `connect` timeout will default to 10 seconds. + /// - redirectConfiguration: Specifies redirect processing settings. If not provided, then it will default to a maximum of 5 follows w/o cycles. + /// - logger: The logger to use for the underlying configuration loaders. + /// - Returns: An instance of KubernetesClientConfig for the Swiftkube KubernetesClient + static func initialize( + timeout: HTTPClient.Configuration.Timeout? = nil, + redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration? = nil, + logger: Logger? + ) throws -> KubernetesClientConfig? { + let kubeConfig: KubeConfig? = { + if let config = try? KubeConfig.fromEnvironment() { + return config + } + + if let config = try? KubeConfig.fromDefaultLocalConfig() { + return config + } + + return try? KubeConfig.fromServiceAccount() + }() + + guard let kubeConfig = kubeConfig else { + return nil } - return try? KubeConfig.fromServiceAccount() - }() - - guard let kubeConfig = kubeConfig else { - return nil + return try from( + kubeConfig: kubeConfig, + contextName: nil, + timeout: timeout, + redirectConfiguration: redirectConfiguration, + logger: logger + ) } - - return try from( - kubeConfig: kubeConfig, - contextName: nil, - timeout: timeout, - redirectConfiguration: redirectConfiguration, - logger: logger - ) - } #endif /// Initializes a client configuration from a given KubeConfig using the specified `current-context`. diff --git a/Sources/SwiftkubeClient/Discovery/KubernetesClient+Discovery.swift b/Sources/SwiftkubeClient/Discovery/KubernetesClient+Discovery.swift index 2fc6475..f48d7cd 100644 --- a/Sources/SwiftkubeClient/Discovery/KubernetesClient+Discovery.swift +++ b/Sources/SwiftkubeClient/Discovery/KubernetesClient+Discovery.swift @@ -126,11 +126,9 @@ internal class DiscoveryClient: DiscoveryAPI, RequestHandlerType { allResourceLists.append(it) } - let merged = allResourceLists.reduce(into: [meta.v1.APIResourceList]()) { (acc, other: meta.v1.APIResourceList) in + return allResourceLists.reduce(into: [meta.v1.APIResourceList]()) { (acc, other: meta.v1.APIResourceList) in acc.append(other) } - - return merged } func serverResources(forGroupVersion groupVersion: String) async throws -> meta.v1.APIResourceList { From 7dc904f5c64d1e282e50477a4f60457505a40f91 Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 19 Mar 2026 11:32:08 +0100 Subject: [PATCH 6/6] use explicit lock/unlock for 5.10 compatibility --- .../Config/KubernetesClientConfig.swift | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift index 3d3712e..66f394e 100644 --- a/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift +++ b/Sources/SwiftkubeClient/Config/KubernetesClientConfig.swift @@ -81,7 +81,9 @@ public final class CachedFileTokenSource: @unchecked Sendable { /// The path to the token file on disk. public var path: String { - lock.withLock { state.path } + lock.lock() + defer { lock.unlock() } + return state.path } private struct State { @@ -104,21 +106,22 @@ public final class CachedFileTokenSource: @unchecked Sendable { /// Returns the current token, re-reading from disk if the cache has expired. public func token() -> String? { - lock.withLock { - let now = NIODeadline.now() - if let cachedToken = state.cachedToken, now < state.expiry { - return cachedToken - } + lock.lock() + defer { lock.unlock() } - guard let newToken = try? String(contentsOfFile: state.path, encoding: .utf8) else { - return nil - } + let now = NIODeadline.now() + if let cachedToken = state.cachedToken, now < state.expiry { + return cachedToken + } - let trimmed = newToken.trimmingCharacters(in: .whitespacesAndNewlines) - state.cachedToken = trimmed - state.expiry = now + state.cacheDuration - return trimmed + guard let newToken = try? String(contentsOfFile: state.path, encoding: .utf8) else { + return nil } + + let trimmed = newToken.trimmingCharacters(in: .whitespacesAndNewlines) + state.cachedToken = trimmed + state.expiry = now + state.cacheDuration + return trimmed } }