diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index fd1247bb5d..a789c282d7 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -23,7 +23,7 @@ Bugfixes: Features: -- android: add support for registering a platform KV store (:issue: `#2134 <2134>`) +- Android & iOS: add support for registering a platform KV store (:issue: `#2134 <2134>`) (:issue: `#2335 <2335>`) - api: add option to extend the keepalive timeout when any frame is received on the owning HTTP/2 connection. (:issue:`#2229 <2229>`) - api: add option to control whether Envoy should drain connections after a soft DNS refresh completes. (:issue:`#2225 <2225>`, :issue:`#2242 <2242>`) - configuration: enable h2 ping by default. (:issue: `#2270 <2270>`) diff --git a/library/objective-c/EnvoyConfiguration.m b/library/objective-c/EnvoyConfiguration.m index db3d68a194..750fb9fede 100644 --- a/library/objective-c/EnvoyConfiguration.m +++ b/library/objective-c/EnvoyConfiguration.m @@ -37,7 +37,10 @@ - (instancetype)initWithAdminInterfaceEnabled:(BOOL)adminInterfaceEnabled (NSArray *)httpPlatformFilterFactories stringAccessors: (NSDictionary *) - stringAccessors { + stringAccessors + keyValueStores: + (NSDictionary> *) + keyValueStores { self = [super init]; if (!self) { return nil; @@ -73,6 +76,7 @@ - (instancetype)initWithAdminInterfaceEnabled:(BOOL)adminInterfaceEnabled self.nativeFilterChain = nativeFilterChain; self.httpPlatformFilterFactories = httpPlatformFilterFactories; self.stringAccessors = stringAccessors; + self.keyValueStores = keyValueStores; return self; } diff --git a/library/objective-c/EnvoyEngine.h b/library/objective-c/EnvoyEngine.h index cc23a80b28..d91ef5e98d 100644 --- a/library/objective-c/EnvoyEngine.h +++ b/library/objective-c/EnvoyEngine.h @@ -315,6 +315,21 @@ extern const int kEnvoyFilterResumeStatusResumeIteration; @end +#pragma mark - EnvoyKeyValueStore + +@protocol EnvoyKeyValueStore + +/// Read a value from the key value store implementation. +- (NSString *_Nullable)readValueForKey:(NSString *)key; + +/// Save a value to the key value store implementation. +- (void)saveValue:(NSString *)value toKey:(NSString *)key; + +/// Remove a value from the key value store implementation. +- (void)removeKey:(NSString *)key; + +@end + #pragma mark - EnvoyNativeFilterConfig @interface EnvoyNativeFilterConfig : NSObject @@ -360,6 +375,7 @@ extern const int kEnvoyFilterResumeStatusResumeIteration; @property (nonatomic, strong) NSArray *nativeFilterChain; @property (nonatomic, strong) NSArray *httpPlatformFilterFactories; @property (nonatomic, strong) NSDictionary *stringAccessors; +@property (nonatomic, strong) NSDictionary> *keyValueStores; /** Create a new instance of the configuration. @@ -397,7 +413,10 @@ extern const int kEnvoyFilterResumeStatusResumeIteration; (NSArray *)httpPlatformFilterFactories stringAccessors: (NSDictionary *) - stringAccessors; + stringAccessors + keyValueStores: + (NSDictionary> *) + keyValueStores; /** Resolves the provided configuration template using properties on this configuration. diff --git a/library/objective-c/EnvoyEngineImpl.m b/library/objective-c/EnvoyEngineImpl.m index b355cdd8db..7ec154e568 100644 --- a/library/objective-c/EnvoyEngineImpl.m +++ b/library/objective-c/EnvoyEngineImpl.m @@ -6,6 +6,7 @@ #import "library/common/main_interface.h" #import "library/common/types/c_types.h" +#import "library/common/extensions/key_value/platform/c_types.h" #if TARGET_OS_IPHONE #import @@ -394,6 +395,46 @@ static envoy_data ios_get_string(const void *context) { return toManagedNativeString(accessor.getEnvoyString()); } +static envoy_data ios_kv_store_read(envoy_data native_key, const void *context) { + // This code block runs inside the Envoy event loop. Therefore, an explicit autoreleasepool block + // is necessary to act as a breaker for any Objective-C allocation that happens. + @autoreleasepool { + id keyValueStore = (__bridge id)context; + NSString *key = [[NSString alloc] initWithBytes:native_key.bytes + length:native_key.length + encoding:NSUTF8StringEncoding]; + NSString *value = [keyValueStore readValueForKey:key]; + return value != nil ? toManagedNativeString(value) : envoy_nodata; + } +} + +static void ios_kv_store_save(envoy_data native_key, envoy_data native_value, const void *context) { + // This code block runs inside the Envoy event loop. Therefore, an explicit autoreleasepool block + // is necessary to act as a breaker for any Objective-C allocation that happens. + @autoreleasepool { + id keyValueStore = (__bridge id)context; + NSString *key = [[NSString alloc] initWithBytes:native_key.bytes + length:native_key.length + encoding:NSUTF8StringEncoding]; + NSString *value = [[NSString alloc] initWithBytes:native_key.bytes + length:native_key.length + encoding:NSUTF8StringEncoding]; + [keyValueStore saveValue:value toKey:key]; + } +} + +static void ios_kv_store_remove(envoy_data native_key, const void *context) { + // This code block runs inside the Envoy event loop. Therefore, an explicit autoreleasepool block + // is necessary to act as a breaker for any Objective-C allocation that happens. + @autoreleasepool { + id keyValueStore = (__bridge id)context; + NSString *key = [[NSString alloc] initWithBytes:native_key.bytes + length:native_key.length + encoding:NSUTF8StringEncoding]; + [keyValueStore removeKey:key]; + } +} + static void ios_track_event(envoy_map map, const void *context) { // This code block runs inside the Envoy event loop. Therefore, an explicit autoreleasepool block // is necessary to act as a breaker for any Objective-C allocation that happens. @@ -492,6 +533,16 @@ - (int)registerStringAccessor:(NSString *)name accessor:(EnvoyStringAccessor *)a return register_platform_api(name.UTF8String, accessorStruct); } +- (int)registerKeyValueStore:(NSString *)name keyValueStore:(id)keyValueStore { + envoy_kv_store *api = safe_malloc(sizeof(envoy_kv_store)); + api->save = ios_kv_store_save; + api->read = ios_kv_store_read; + api->remove = ios_kv_store_remove; + api->context = CFBridgingRetain(keyValueStore); + + return register_platform_api(name.UTF8String, api); +} + - (int)runWithConfig:(EnvoyConfiguration *)config logLevel:(NSString *)logLevel { NSString *templateYAML = [[NSString alloc] initWithUTF8String:config_template]; return [self runWithTemplate:templateYAML config:config logLevel:logLevel]; diff --git a/library/swift/BUILD b/library/swift/BUILD index 4895734390..568cfb68f5 100644 --- a/library/swift/BUILD +++ b/library/swift/BUILD @@ -17,6 +17,7 @@ swift_library( "FinalStreamIntel.swift", "Headers.swift", "HeadersBuilder.swift", + "KeyValueStore.swift", "LogLevel.swift", "PulseClient.swift", "PulseClientImpl.swift", diff --git a/library/swift/EngineBuilder.swift b/library/swift/EngineBuilder.swift index d0a0ba99b1..173d521be6 100644 --- a/library/swift/EngineBuilder.swift +++ b/library/swift/EngineBuilder.swift @@ -44,6 +44,7 @@ open class EngineBuilder: NSObject { private var nativeFilterChain: [EnvoyNativeFilterConfig] = [] private var platformFilterChain: [EnvoyHTTPFilterFactory] = [] private var stringAccessors: [String: EnvoyStringAccessor] = [:] + private var keyValueStores: [String: EnvoyKeyValueStore] = [:] private var directResponses: [DirectResponse] = [] // MARK: - Public @@ -348,13 +349,25 @@ open class EngineBuilder: NSObject { /// - parameter name: the name of the accessor. /// - parameter accessor: lambda to access a string from the platform layer. /// - /// - returns this builder. + /// - returns: This builder. @discardableResult public func addStringAccessor(name: String, accessor: @escaping () -> String) -> Self { self.stringAccessors[name] = EnvoyStringAccessor(block: accessor) return self } + /// Register a key-value store implementation for internal use. + /// + /// - parameter name: the name of the KV store. + /// - parameter keyValueStore: the KV store implementation. + /// + /// - returns: This builder. + @discardableResult + public func addKeyValueStore(name: String, keyValueStore: KeyValueStore) -> Self { + self.keyValueStores[name] = KeyValueStoreImpl(implementation: keyValueStore) + return self + } + /// Set a closure to be called when the engine finishes its async startup and begins running. /// /// - parameter closure: The closure to be called. @@ -482,7 +495,8 @@ open class EngineBuilder: NSObject { .joined(separator: "\n"), nativeFilterChain: self.nativeFilterChain, platformFilterChain: self.platformFilterChain, - stringAccessors: self.stringAccessors + stringAccessors: self.stringAccessors, + keyValueStores: self.keyValueStores ) switch self.base { diff --git a/library/swift/KeyValueStore.swift b/library/swift/KeyValueStore.swift new file mode 100644 index 0000000000..954b1e7302 --- /dev/null +++ b/library/swift/KeyValueStore.swift @@ -0,0 +1,36 @@ +@_implementationOnly import EnvoyEngine +import Foundation + +/// `KeyValueStore` is an interface that may be implemented to provide access to an arbitrary +/// key-value store implementation that may be made accessible to native Envoy Mobile code. +public protocol KeyValueStore { + /// Read a value from the key value store implementation. + func readValue(forKey key: String) -> String? + + /// Save a value to the key value store implementation. + func saveValue(_ value: String, toKey key: String) + + /// Remove a value from the key value store implementation. + func removeKey(_ key: String) +} + +/// KeyValueStoreImpl is an internal type used for mapping calls from the common library layer. +internal class KeyValueStoreImpl: EnvoyKeyValueStore { + internal let implementation: KeyValueStore + + init(implementation: KeyValueStore) { + self.implementation = implementation + } + + func readValue(forKey key: String) -> String? { + return implementation.readValue(forKey: key) + } + + func saveValue(_ value: String, toKey key: String) { + implementation.saveValue(value, toKey: key) + } + + func removeKey(_ key: String) { + implementation.removeKey(key) + } +} diff --git a/test/swift/EngineBuilderTests.swift b/test/swift/EngineBuilderTests.swift index b050a5c53a..95af4ac058 100644 --- a/test/swift/EngineBuilderTests.swift +++ b/test/swift/EngineBuilderTests.swift @@ -418,6 +418,42 @@ final class EngineBuilderTests: XCTestCase { self.waitForExpectations(timeout: 0.01) } + func testAddingKeyValueStoreToConfigurationWhenRunningEnvoy() { + let expectation = self.expectation(description: "Run called with expected data") + MockEnvoyEngine.onRunWithConfig = { config, _ in + XCTAssertEqual("bar", config.keyValueStores["name"]?.readValue(forKey: "foo")) + expectation.fulfill() + } + + let testStore: KeyValueStore = { + class TestStore: KeyValueStore { + private var dict: [String: String] = [:] + + func readValue(forKey key: String) -> String? { + return dict[key] + } + + func saveValue(_ value: String, toKey key: String) { + dict[key] = value + } + + func removeKey(_ key: String) { + dict[key] = nil + } + } + + return TestStore() + }() + + testStore.saveValue("bar", toKey: "foo") + + _ = EngineBuilder() + .addEngineType(MockEnvoyEngine.self) + .addKeyValueStore(name: "name", keyValueStore: testStore) + .build() + self.waitForExpectations(timeout: 0.01) + } + func testResolvesYAMLWithIndividuallySetValues() throws { let config = EnvoyConfiguration( adminInterfaceEnabled: false, @@ -452,7 +488,8 @@ final class EngineBuilderTests: XCTestCase { platformFilterChain: [ EnvoyHTTPFilterFactory(filterName: "TestFilter", factory: TestFilter.init), ], - stringAccessors: [:] + stringAccessors: [:], + keyValueStores: [:] ) let resolvedYAML = try XCTUnwrap(config.resolveTemplate(kMockTemplate)) XCTAssertTrue(resolvedYAML.contains("&connect_timeout 200s")) @@ -532,7 +569,8 @@ final class EngineBuilderTests: XCTestCase { platformFilterChain: [ EnvoyHTTPFilterFactory(filterName: "TestFilter", factory: TestFilter.init), ], - stringAccessors: [:] + stringAccessors: [:], + keyValueStores: [:] ) let resolvedYAML = try XCTUnwrap(config.resolveTemplate(kMockTemplate)) XCTAssertTrue(resolvedYAML.contains("&dns_lookup_family V4_PREFERRED")) @@ -573,7 +611,8 @@ final class EngineBuilderTests: XCTestCase { directResponses: "", nativeFilterChain: [], platformFilterChain: [], - stringAccessors: [:] + stringAccessors: [:], + keyValueStores: [:] ) XCTAssertNil(config.resolveTemplate("{{ missing }}")) }