Parcourir la source

Add IPv4 and IPv6 resolvers (#1810)

Motivation:

Users must be able to specify already-resolved IPv4 and IPv6 addresses
to the name resolver registry and have it return a name resolver. These
resolvers are effectively passthrough resolvers which attach a service
configuration to the provided addresses.

Modifications:

- Add IPv4 and IPv6 targets and resolvers

Result:

- Name resolver factory can resolve IP addresses
George Barnett il y a 1 an
Parent
commit
79a27eb726

+ 75 - 0
Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift

@@ -0,0 +1,75 @@
+/*
+ * 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 IPv4 addresses.
+  ///
+  /// IPv4 addresses can be resolved by the ``NameResolvers/IPv4`` resolver which creates a
+  /// separate ``Endpoint`` for each address.
+  public struct IPv4: ResolvableTarget {
+    /// The IPv4 addresses.
+    public var addresses: [SocketAddress.IPv4]
+
+    /// Create a new IPv4 target.
+    /// - Parameter addresses: The IPv4 addresses.
+    public init(addresses: [SocketAddress.IPv4]) {
+      self.addresses = addresses
+    }
+  }
+}
+
+extension ResolvableTarget where Self == ResolvableTargets.IPv4 {
+  /// Creates a new resolvable IPv4 target for a single address.
+  /// - Parameters:
+  ///   - host: The host address.
+  ///   - port: The port on the host.
+  /// - Returns: A ``ResolvableTarget``.
+  public static func ipv4(host: String, port: Int = 443) -> Self {
+    let address = SocketAddress.IPv4(host: host, port: port)
+    return Self(addresses: [address])
+  }
+
+  /// Creates a new resolvable IPv4 target from the provided host-port pairs.
+  ///
+  /// - Parameter pairs: An array of host-port pairs.
+  /// - Returns: A ``ResolvableTarget``.
+  public static func ipv4(pairs: [(host: String, port: Int)]) -> Self {
+    let address = pairs.map { SocketAddress.IPv4(host: $0.host, port: $0.port) }
+    return Self(addresses: address)
+  }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension NameResolvers {
+  /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv4`` targets.
+  ///
+  /// The name resolver for a given target always produces the same values, with one endpoint per
+  /// address in the target. This resolver doesn't support fetching service configuration.
+  public struct IPv4: NameResolverFactory {
+    public typealias Target = ResolvableTargets.IPv4
+
+    /// Create a new IPv4 resolver factory.
+    public init() {}
+
+    public func resolver(for target: Target) -> NameResolver {
+      let endpoints = target.addresses.map { Endpoint(addresses: [.ipv4($0)]) }
+      let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfiguration: nil)
+      return NameResolver(names: .constant(resolutionResult), updateMode: .pull)
+    }
+  }
+}

+ 75 - 0
Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift

@@ -0,0 +1,75 @@
+/*
+ * 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 IPv4 addresses.
+  ///
+  /// IPv4 addresses can be resolved by the ``NameResolvers/IPv6`` resolver which creates a
+  /// separate ``Endpoint`` for each address.
+  public struct IPv6: ResolvableTarget {
+    /// The IPv6 addresses.
+    public var addresses: [SocketAddress.IPv6]
+
+    /// Create a new IPv6 target.
+    /// - Parameter addresses: The IPv6 addresses.
+    public init(addresses: [SocketAddress.IPv6]) {
+      self.addresses = addresses
+    }
+  }
+}
+
+extension ResolvableTarget where Self == ResolvableTargets.IPv6 {
+  /// Creates a new resolvable IPv6 target for a single address.
+  /// - Parameters:
+  ///   - host: The host address.
+  ///   - port: The port on the host.
+  /// - Returns: A ``ResolvableTarget``.
+  public static func ipv6(host: String, port: Int = 443) -> Self {
+    let address = SocketAddress.IPv6(host: host, port: port)
+    return Self(addresses: [address])
+  }
+
+  /// Creates a new resolvable IPv6 target from the provided host-port pairs.
+  ///
+  /// - Parameter pairs: An array of host-port pairs.
+  /// - Returns: A ``ResolvableTarget``.
+  public static func ipv6(pairs: [(host: String, port: Int)]) -> Self {
+    let address = pairs.map { SocketAddress.IPv6(host: $0.host, port: $0.port) }
+    return Self(addresses: address)
+  }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension NameResolvers {
+  /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv6`` targets.
+  ///
+  /// The name resolver for a given target always produces the same values, with one endpoint per
+  /// address in the target. This resolver doesn't support fetching service configuration.
+  public struct IPv6: NameResolverFactory {
+    public typealias Target = ResolvableTargets.IPv6
+
+    /// Create a new IPv6 resolver factory.
+    public init() {}
+
+    public func resolver(for target: Target) -> NameResolver {
+      let endpoints = target.addresses.map { Endpoint(addresses: [.ipv6($0)]) }
+      let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfiguration: nil)
+      return NameResolver(names: .constant(resolutionResult), updateMode: .pull)
+    }
+  }
+}

+ 11 - 4
Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift

@@ -52,8 +52,8 @@ public struct NameResolver: Sendable {
   }
 
   /// Create a new name resolver.
-  public init(results: RPCAsyncSequence<NameResolutionResult>, updateMode: UpdateMode) {
-    self.names = results
+  public init(names: RPCAsyncSequence<NameResolutionResult>, updateMode: UpdateMode) {
+    self.names = names
     self.updateMode = updateMode
   }
 }
@@ -66,11 +66,12 @@ public struct NameResolutionResult: Hashable, Sendable {
   public var endpoints: [Endpoint]
 
   /// The service configuration reported by the resolver, or an error if it couldn't be parsed.
-  public var serviceConfiguration: Result<ServiceConfiguration, RPCError>
+  /// This value may be `nil` if the resolver doesn't support fetching service configuration.
+  public var serviceConfiguration: Result<ServiceConfiguration, RPCError>?
 
   public init(
     endpoints: [Endpoint],
-    serviceConfiguration: Result<ServiceConfiguration, RPCError>
+    serviceConfiguration: Result<ServiceConfiguration, RPCError>?
   ) {
     self.endpoints = endpoints
     self.serviceConfiguration = serviceConfiguration
@@ -127,3 +128,9 @@ extension NameResolverFactory {
 
 /// A target which can be resolved to a ``SocketAddress``.
 public protocol ResolvableTarget {}
+
+/// A namespace for resolvable targets.
+public enum ResolvableTargets {}
+
+/// A namespace for name resolver factories.
+public enum NameResolvers {}

+ 29 - 2
Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift

@@ -40,14 +40,26 @@
 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
 public struct NameResolverRegistry {
   private enum Factory {
+    case ipv4(NameResolvers.IPv4)
+    case ipv6(NameResolvers.IPv6)
     case other(any NameResolverFactory)
 
     init(_ factory: some NameResolverFactory) {
-      self = .other(factory)
+      if let ipv4 = factory as? NameResolvers.IPv4 {
+        self = .ipv4(ipv4)
+      } else if let ipv6 = factory as? NameResolvers.IPv6 {
+        self = .ipv6(ipv6)
+      } else {
+        self = .other(factory)
+      }
     }
 
     func makeResolverIfCompatible<Target: ResolvableTarget>(_ target: Target) -> NameResolver? {
       switch self {
+      case .ipv4(let factory):
+        return factory.makeResolverIfCompatible(target)
+      case .ipv6(let factory):
+        return factory.makeResolverIfCompatible(target)
       case .other(let factory):
         return factory.makeResolverIfCompatible(target)
       }
@@ -55,6 +67,10 @@ public struct NameResolverRegistry {
 
     func hasTarget<Target: ResolvableTarget>(_ target: Target) -> Bool {
       switch self {
+      case .ipv4(let factory):
+        return factory.isCompatible(withTarget: target)
+      case .ipv6(let factory):
+        return factory.isCompatible(withTarget: target)
       case .other(let factory):
         return factory.isCompatible(withTarget: target)
       }
@@ -62,6 +78,10 @@ public struct NameResolverRegistry {
 
     func `is`<Factory: NameResolverFactory>(ofType factoryType: Factory.Type) -> Bool {
       switch self {
+      case .ipv4:
+        return NameResolvers.IPv4.self == factoryType
+      case .ipv6:
+        return NameResolvers.IPv6.self == factoryType
       case .other(let factory):
         return type(of: factory) == factoryType
       }
@@ -76,8 +96,15 @@ public struct NameResolverRegistry {
   }
 
   /// Returns a new name resolver registry with the default factories registered.
+  ///
+  /// The default resolvers include:
+  /// - ``NameResolvers/IPv4``,
+  /// - ``NameResolvers/IPv6``.
   public static var defaults: Self {
-    return NameResolverRegistry()
+    var resolvers = NameResolverRegistry()
+    resolvers.registerFactory(NameResolvers.IPv4())
+    resolvers.registerFactory(NameResolvers.IPv6())
+    return resolvers
   }
 
   /// The number of resolver factories in the registry.

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

@@ -0,0 +1,48 @@
+/*
+ * 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
+
+private struct ConstantAsyncSequence<Element: Sendable>: AsyncSequence {
+  private let element: Element
+
+  init(element: Element) {
+    self.element = element
+  }
+
+  func makeAsyncIterator() -> AsyncIterator {
+    return AsyncIterator(element: self.element)
+  }
+
+  struct AsyncIterator: AsyncIteratorProtocol {
+    private let element: Element
+
+    fileprivate init(element: Element) {
+      self.element = element
+    }
+
+    func next() async throws -> Element? {
+      return self.element
+    }
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension RPCAsyncSequence {
+  static func constant(_ element: Element) -> RPCAsyncSequence<Element> {
+    return RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: element))
+  }
+}

+ 107 - 1
Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift

@@ -34,7 +34,7 @@ final class NameResolverRegistryTests: XCTestCase {
         $0.yield(with: .failure(RPCError(code: self.code, message: target.value)))
       }
 
-      return NameResolver(results: RPCAsyncSequence(wrapping: stream), updateMode: .pull)
+      return NameResolver(names: RPCAsyncSequence(wrapping: stream), updateMode: .pull)
     }
   }
 
@@ -129,4 +129,110 @@ final class NameResolverRegistryTests: XCTestCase {
       XCTAssertEqual(error.message, "foo")
     }
   }
+
+  func testDefaultResolvers() {
+    let resolvers = NameResolverRegistry.defaults
+    XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self))
+    XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self))
+  }
+
+  func testMakeResolver() {
+    let resolvers = NameResolverRegistry()
+    XCTAssertNil(resolvers.makeResolver(for: .ipv4(host: "foo")))
+  }
+
+  func testCustomResolver() async throws {
+    struct EmptyTarget: ResolvableTarget {
+      static var scheme: String { "empty" }
+    }
+
+    struct CustomResolver: NameResolverFactory {
+      func resolver(for target: EmptyTarget) -> NameResolver {
+        return NameResolver(
+          names: RPCAsyncSequence(wrapping: AsyncStream { $0.finish() }),
+          updateMode: .push
+        )
+      }
+    }
+
+    var resolvers = NameResolverRegistry.defaults
+    resolvers.registerFactory(CustomResolver())
+    let resolver = try XCTUnwrap(resolvers.makeResolver(for: EmptyTarget()))
+    XCTAssertEqual(resolver.updateMode, .push)
+    for try await _ in resolver.names {
+      XCTFail("Expected an empty sequence")
+    }
+  }
+
+  func testIPv4ResolverForSingleHost() async throws {
+    let factory = NameResolvers.IPv4()
+    let resolver = factory.resolver(for: .ipv4(host: "foo", port: 1234))
+
+    XCTAssertEqual(resolver.updateMode, .pull)
+
+    // The IPv4 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: [.ipv4(host: "foo", port: 1234)])])
+      XCTAssertNil(result.serviceConfiguration)
+    }
+  }
+
+  func testIPv4ResolverForMultipleHosts() async throws {
+    let factory = NameResolvers.IPv4()
+    let resolver = factory.resolver(for: .ipv4(pairs: [("foo", 443), ("bar", 444)]))
+
+    XCTAssertEqual(resolver.updateMode, .pull)
+
+    // The IPv4 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: [.ipv4(host: "foo", port: 443)]),
+          Endpoint(addresses: [.ipv4(host: "bar", port: 444)]),
+        ]
+      )
+      XCTAssertNil(result.serviceConfiguration)
+    }
+  }
+
+  func testIPv6ResolverForSingleHost() async throws {
+    let factory = NameResolvers.IPv6()
+    let resolver = factory.resolver(for: .ipv6(host: "foo", port: 1234))
+
+    XCTAssertEqual(resolver.updateMode, .pull)
+
+    // The IPv6 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: [.ipv6(host: "foo", port: 1234)])])
+      XCTAssertNil(result.serviceConfiguration)
+    }
+  }
+
+  func testIPv6ResolverForMultipleHosts() async throws {
+    let factory = NameResolvers.IPv6()
+    let resolver = factory.resolver(for: .ipv6(pairs: [("foo", 443), ("bar", 444)]))
+
+    XCTAssertEqual(resolver.updateMode, .pull)
+
+    // The IPv6 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: [.ipv6(host: "foo", port: 443)]),
+          Endpoint(addresses: [.ipv6(host: "bar", port: 444)]),
+        ]
+      )
+      XCTAssertNil(result.serviceConfiguration)
+    }
+  }
 }