Browse Source

Add UnixDomainSocket resolver (#1814)

Motivation:

Users should be able to connect to Unix Domain Sockets. To do these they
need a UDS target and resolver.

Modifications:

- Add a UDS target and resolver
- Update the registry to include it by default

Result:

Can resolve UDS targets
George Barnett 1 year ago
parent
commit
7469629ce5

+ 60 - 0
Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024, gRPC 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
+ *
+ *     http://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 GRPCCore
+
+extension ResolvableTargets {
+  /// A resolvable target for Unix Domain Socket address.
+  ///
+  /// ``UnixDomainSocket`` addresses can be resolved by the ``NameResolvers/UnixDomainSocket``
+  /// resolver which creates a single ``Endpoint`` for target address.
+  public struct UnixDomainSocket: ResolvableTarget {
+    /// The Unix Domain Socket address.
+    public var address: SocketAddress.UnixDomainSocket
+
+    /// Create a new Unix Domain Socket address.
+    public init(address: SocketAddress.UnixDomainSocket) {
+      self.address = address
+    }
+  }
+}
+
+extension ResolvableTarget where Self == ResolvableTargets.UnixDomainSocket {
+  /// Creates a new resolvable Unix Domain Socket target.
+  /// - Parameter path: The path of the socket.
+  public static func unixDomainSocket(path: String) -> Self {
+    return Self(address: SocketAddress.UnixDomainSocket(path: path))
+  }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension NameResolvers {
+  /// A ``NameResolverFactory`` for ``ResolvableTargets/UnixDomainSocket`` targets.
+  ///
+  /// The name resolver for a given target always produces the same values, with a single endpoint.
+  /// This resolver doesn't support fetching service configuration.
+  public struct UnixDomainSocket: NameResolverFactory {
+    public typealias Target = ResolvableTargets.UnixDomainSocket
+
+    public init() {}
+
+    public func resolver(for target: Target) -> NameResolver {
+      let endpoint = Endpoint(addresses: [.unixDomainSocket(target.address)])
+      let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfiguration: nil)
+      return NameResolver(names: .constant(resolutionResult), updateMode: .pull)
+    }
+  }
+}

+ 12 - 1
Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift

@@ -42,6 +42,7 @@ public struct NameResolverRegistry {
   private enum Factory {
     case ipv4(NameResolvers.IPv4)
     case ipv6(NameResolvers.IPv6)
+    case unix(NameResolvers.UnixDomainSocket)
     case other(any NameResolverFactory)
 
     init(_ factory: some NameResolverFactory) {
@@ -49,6 +50,8 @@ public struct NameResolverRegistry {
         self = .ipv4(ipv4)
       } else if let ipv6 = factory as? NameResolvers.IPv6 {
         self = .ipv6(ipv6)
+      } else if let unix = factory as? NameResolvers.UnixDomainSocket {
+        self = .unix(unix)
       } else {
         self = .other(factory)
       }
@@ -60,6 +63,8 @@ public struct NameResolverRegistry {
         return factory.makeResolverIfCompatible(target)
       case .ipv6(let factory):
         return factory.makeResolverIfCompatible(target)
+      case .unix(let factory):
+        return factory.makeResolverIfCompatible(target)
       case .other(let factory):
         return factory.makeResolverIfCompatible(target)
       }
@@ -71,6 +76,8 @@ public struct NameResolverRegistry {
         return factory.isCompatible(withTarget: target)
       case .ipv6(let factory):
         return factory.isCompatible(withTarget: target)
+      case .unix(let factory):
+        return factory.isCompatible(withTarget: target)
       case .other(let factory):
         return factory.isCompatible(withTarget: target)
       }
@@ -82,6 +89,8 @@ public struct NameResolverRegistry {
         return NameResolvers.IPv4.self == factoryType
       case .ipv6:
         return NameResolvers.IPv6.self == factoryType
+      case .unix:
+        return NameResolvers.UnixDomainSocket.self == factoryType
       case .other(let factory):
         return type(of: factory) == factoryType
       }
@@ -99,11 +108,13 @@ public struct NameResolverRegistry {
   ///
   /// The default resolvers include:
   /// - ``NameResolvers/IPv4``,
-  /// - ``NameResolvers/IPv6``.
+  /// - ``NameResolvers/IPv6``,
+  /// - ``NameResolvers/UnixDomainSocket``.
   public static var defaults: Self {
     var resolvers = NameResolverRegistry()
     resolvers.registerFactory(NameResolvers.IPv4())
     resolvers.registerFactory(NameResolvers.IPv6())
+    resolvers.registerFactory(NameResolvers.UnixDomainSocket())
     return resolvers
   }
 

+ 1 - 0
Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift

@@ -16,6 +16,7 @@
 
 import GRPCCore
 
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
 private struct ConstantAsyncSequence<Element: Sendable>: AsyncSequence {
   private let element: Element
 

+ 17 - 0
Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift

@@ -134,6 +134,8 @@ final class NameResolverRegistryTests: XCTestCase {
     let resolvers = NameResolverRegistry.defaults
     XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self))
     XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self))
+    XCTAssert(resolvers.containsFactory(ofType: NameResolvers.UnixDomainSocket.self))
+    XCTAssertEqual(resolvers.count, 3)
   }
 
   func testMakeResolver() {
@@ -235,4 +237,19 @@ final class NameResolverRegistryTests: XCTestCase {
       XCTAssertNil(result.serviceConfiguration)
     }
   }
+
+  func testUDSResolver() async throws {
+    let factory = NameResolvers.UnixDomainSocket()
+    let resolver = factory.resolver(for: .unixDomainSocket(path: "/foo"))
+
+    XCTAssertEqual(resolver.updateMode, .pull)
+
+    // The UDS resolver always returns the same values.
+    var iterator = resolver.names.makeAsyncIterator()
+    for _ in 0 ..< 1000 {
+      let result = try await XCTUnwrapAsync { try await iterator.next() }
+      XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.unixDomainSocket(path: "/foo")])])
+      XCTAssertNil(result.serviceConfiguration)
+    }
+  }
 }