-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ios: implement Envoy builder & add tests (#343)
Implements a new `EnvoyBuilder` type which consumers will now use to create new instances of `Envoy` going forward (rather than initializing it directly). This supports the following using a builder pattern: - Adding log levels - Adding configuration options that were previously part of `Configuration` - Specifying a YAML file override for consumers who may want to use a custom configuration instead of the default template - Creating an instance of `Envoy` with these configurations using an underlying `EnvoyEngine` implementation - Injecting mock engines so we don't start real instances of Envoy when testing Example usage: ```swift let envoy = try EnvoyBuilder() .addStatsFlushSeconds(30) .addDNSRefreshSeconds(30) .addLogLevel(.trace) .build() ``` Signed-off-by: Michael Rebello <[email protected]> Signed-off-by: JP Simard <[email protected]>
- Loading branch information
Showing
9 changed files
with
249 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import Foundation | ||
|
||
/// Error that may be thrown by the Envoy builder. | ||
@objc | ||
public enum EnvoyBuilderError: Int, Swift.Error { | ||
/// Not all keys within the provided template were resolved. | ||
case unresolvedTemplateKey | ||
} | ||
|
||
/// Builder used for creating new instances of Envoy. | ||
@objcMembers | ||
public final class EnvoyBuilder: NSObject { | ||
private var engineType: EnvoyEngine.Type = EnvoyEngineImpl.self | ||
private var logLevel: LogLevel = .info | ||
private var configYAML: String? | ||
|
||
private var connectTimeoutSeconds: UInt = 30 | ||
private var dnsRefreshSeconds: UInt = 60 | ||
private var statsFlushSeconds: UInt = 60 | ||
|
||
// MARK: - Public | ||
|
||
/// Add a log level to use with Envoy. | ||
public func addLogLevel(_ logLevel: LogLevel) -> EnvoyBuilder { | ||
self.logLevel = logLevel | ||
return self | ||
} | ||
|
||
// MARK: - YAML configuration options | ||
|
||
/// Add a YAML file to use as a configuration. | ||
/// Setting this will supersede any other configuration settings in the builder. | ||
@discardableResult | ||
public func addConfigYAML(_ configYAML: String?) -> EnvoyBuilder { | ||
self.configYAML = configYAML | ||
return self | ||
} | ||
|
||
/// Add a timeout for new network connections to hosts in the cluster. | ||
@discardableResult | ||
public func addConnectTimeoutSeconds(_ connectTimeoutSeconds: UInt) -> EnvoyBuilder { | ||
self.connectTimeoutSeconds = connectTimeoutSeconds | ||
return self | ||
} | ||
|
||
/// Add a rate at which to refresh DNS. | ||
@discardableResult | ||
public func addDNSRefreshSeconds(_ dnsRefreshSeconds: UInt) -> EnvoyBuilder { | ||
self.dnsRefreshSeconds = dnsRefreshSeconds | ||
return self | ||
} | ||
|
||
/// Add an interval at which to flush Envoy stats. | ||
@discardableResult | ||
public func addStatsFlushSeconds(_ statsFlushSeconds: UInt) -> EnvoyBuilder { | ||
self.statsFlushSeconds = statsFlushSeconds | ||
return self | ||
} | ||
|
||
/// Builds a new instance of Envoy using the provided configurations. | ||
/// | ||
/// - returns: A new instance of Envoy. | ||
public func build() throws -> Envoy { | ||
let engine = self.engineType.init() | ||
let configYAML = try self.configYAML ?? self.resolvedYAML() | ||
return Envoy(configYAML: configYAML, logLevel: self.logLevel, engine: engine) | ||
} | ||
|
||
// MARK: - Internal | ||
|
||
/// Add a specific implementation of `EnvoyEngine` to use for starting Envoy. | ||
/// A new instance of this engine will be created when `build()` is called. | ||
/// Used for testing, as initializing with `EnvoyEngine.Type` results in a | ||
/// segfault: https://github.com/lyft/envoy-mobile/issues/334 | ||
@discardableResult | ||
func addEngineType(_ engineType: EnvoyEngine.Type) -> EnvoyBuilder { | ||
self.engineType = engineType | ||
return self | ||
} | ||
|
||
/// Processes the YAML template provided, replacing keys with values from the configuration. | ||
/// | ||
/// - parameter template: The template YAML file to use. | ||
/// | ||
/// - returns: A resolved YAML file with all template keys replaced. | ||
func resolvedYAML(_ template: String = EnvoyConfiguration.templateString()) throws -> String { | ||
var template = template | ||
let templateKeysToValues: [String: String] = [ | ||
"connect_timeout": "\(self.connectTimeoutSeconds)s", | ||
"dns_refresh_rate": "\(self.dnsRefreshSeconds)s", | ||
"stats_flush_interval": "\(self.statsFlushSeconds)s", | ||
] | ||
|
||
for (templateKey, value) in templateKeysToValues { | ||
while let range = template.range(of: "{{ \(templateKey) }}") { | ||
template = template.replacingCharacters(in: range, with: value) | ||
} | ||
} | ||
|
||
if template.contains("{{") { | ||
throw EnvoyBuilderError.unresolvedTemplateKey | ||
} | ||
|
||
return template | ||
} | ||
} | ||
|
||
// MARK: - Objective-C helpers | ||
|
||
extension Envoy { | ||
/// Convenience builder function to allow for cleaner Objective-C syntax. | ||
/// | ||
/// For example: | ||
/// | ||
/// Envoy *envoy = [EnvoyBuilder withBuild:^(EnvoyBuilder *builder) { | ||
/// [builder addDNSRefreshSeconds:30]; | ||
/// }]; | ||
@objc | ||
public static func with(build: (EnvoyBuilder) -> Void) throws -> Envoy { | ||
let builder = EnvoyBuilder() | ||
build(builder) | ||
return try builder.build() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
@testable import Envoy | ||
import Foundation | ||
import XCTest | ||
|
||
private let kMockTemplate = """ | ||
mock_template: | ||
- name: mock | ||
connect_timeout: {{ connect_timeout }} | ||
dns_refresh_rate: {{ dns_refresh_rate }} | ||
stats_flush_interval: {{ stats_flush_interval }} | ||
""" | ||
|
||
private final class MockEnvoyEngine: NSObject, EnvoyEngine { | ||
static var onRun: ((_ config: String, _ logLevel: String?) -> Void)? | ||
|
||
func run(withConfig config: String) -> Int32 { | ||
MockEnvoyEngine.onRun?(config, nil) | ||
return 0 | ||
} | ||
|
||
func run(withConfig config: String, logLevel: String) -> Int32 { | ||
MockEnvoyEngine.onRun?(config, logLevel) | ||
return 0 | ||
} | ||
|
||
func setup() {} | ||
|
||
func startStream(with observer: EnvoyObserver) -> EnvoyHTTPStream { | ||
return EnvoyHTTPStream() | ||
} | ||
} | ||
|
||
final class EnvoyBuilderTests: XCTestCase { | ||
override func tearDown() { | ||
super.tearDown() | ||
MockEnvoyEngine.onRun = nil | ||
} | ||
|
||
func testAddingCustomConfigYAMLUsesSpecifiedYAMLWhenRunningEnvoy() throws { | ||
let expectation = self.expectation(description: "Run called with expected data") | ||
MockEnvoyEngine.onRun = { yaml, _ in | ||
XCTAssertEqual("foobar", yaml) | ||
expectation.fulfill() | ||
} | ||
|
||
_ = try EnvoyBuilder() | ||
.addEngineType(MockEnvoyEngine.self) | ||
.addConfigYAML("foobar") | ||
.build() | ||
self.waitForExpectations(timeout: 0.01) | ||
} | ||
|
||
func testAddingLogLevelAddsLogLevelWhenRunningEnvoy() throws { | ||
let expectation = self.expectation(description: "Run called with expected data") | ||
MockEnvoyEngine.onRun = { _, logLevel in | ||
XCTAssertEqual("trace", logLevel) | ||
expectation.fulfill() | ||
} | ||
|
||
_ = try EnvoyBuilder() | ||
.addEngineType(MockEnvoyEngine.self) | ||
.addLogLevel(.trace) | ||
.build() | ||
self.waitForExpectations(timeout: 0.01) | ||
} | ||
|
||
func testResolvesYAMLWithConnectTimeout() throws { | ||
let resolvedYAML = try EnvoyBuilder() | ||
.addEngineType(MockEnvoyEngine.self) | ||
.addConnectTimeoutSeconds(200) | ||
.resolvedYAML(kMockTemplate) | ||
|
||
XCTAssertTrue(resolvedYAML.contains("connect_timeout: 200s")) | ||
} | ||
|
||
func testResolvesYAMLWithDNSRefreshSeconds() throws { | ||
let resolvedYAML = try EnvoyBuilder() | ||
.addEngineType(MockEnvoyEngine.self) | ||
.addDNSRefreshSeconds(200) | ||
.resolvedYAML(kMockTemplate) | ||
|
||
XCTAssertTrue(resolvedYAML.contains("dns_refresh_rate: 200s")) | ||
} | ||
|
||
func testResolvesYAMLWithStatsFlushSeconds() throws { | ||
let resolvedYAML = try EnvoyBuilder() | ||
.addEngineType(MockEnvoyEngine.self) | ||
.addStatsFlushSeconds(200) | ||
.resolvedYAML(kMockTemplate) | ||
|
||
XCTAssertTrue(resolvedYAML.contains("stats_flush_interval: 200s")) | ||
} | ||
|
||
func testThrowsWhenUnresolvedValueInTemplate() { | ||
let builder = EnvoyBuilder().addEngineType(MockEnvoyEngine.self) | ||
XCTAssertThrowsError(try builder.resolvedYAML("{{ missing }}")) { error in | ||
XCTAssertEqual(.unresolvedTemplateKey, error as? EnvoyBuilderError) | ||
} | ||
} | ||
} |