- Creating fine-grained providers per Vault engine, each handling its own parameters. Still uses the context.
// Init rest client
let restClient = VaultClient(...)
// Provider of Vault key-value secret engine
let keyValueSecretProvider = KeyValueSecretProvider(
client: restClient,
mount: "path/to/kv_secret/mount"
)
// Provider of Vault database secret engine
let databaseSecretProvider = DatabaseSecretProvider(
client: restClient,
mount: "path/to/database/mount"
)
let config = ConfigReader(providers: [
keyValueSecretProvider,
databaseSecretProvider,
try await JSONProvider(filePath: .init("config.json")),
CustomEnvironmentVariablesProvider() // Uses the context, unlike EnvironmentVariablesProvider
])
// - could be read from `keyValueSecretProvider`
// - always fails with `databaseSecretProvider` as the "keyName" and "version" contexts are not defined for that secret engine
// - could be read from the json provider
// - could be read from a custom env var provider THIRD_PARTY_SERVICE_API_KEY_KEY_NAME_DEV_VERSION_2
let apiKey = try await sut.fetchString(
forKey: "third_party.service.api_key",
context: ["keyName": "dev", "version": 2]
)
// - always fails with `keyValueSecretProvider` as the "type" and "roleName" contexts are not defined for that secret engine
// - could be read from `databaseSecretProvider`
// - could be read from the json provider
// - could be read from the env var DATABASE_PASSWORD_TYPE_DYNAMIC_ROLENAME_QA
let password = try await sut.fetchRequiredString(
forKey: "database.password",
context: ["type": "dynamic", "roleName": "qa"]
)
- Building a mutable VaultSecretProvider that maps config keys to REST client operations.
// Init rest client
let vaultClient = VaultClient(...)
let absoluteKey1 = AbsoluteConfigKey(["third_party", "service", "api_key"])
let absoluteKey2 = AbsoluteConfigKey(["third_party", "service", "api_key"], context: ["version": 2])
let absoluteKey3 = AbsoluteConfigKey(["server", "database", "credentials"])
let secretProvider = VaultSecretProvider(
vaultClient: vaultClient,
evaluationMap: [
absoluteKey1: try await VaultSecretProvider.keyValueSecret(mount: kvMount, key: secretKeyPath),
absoluteKey2: try await VaultSecretProvider.keyValueSecret(mount: kvMount, key: secretKeyPath, version: 2),
absoluteKey3: try await VaultSecretProvider.databaseCredentials(mount: databaseMount, role: .static(name: staticRole))
]
)
let config = ConfigReader(providers: [
secretProvider,
try await JSONProvider(filePath: .init(config2.json))
])
// Secret provider will be used inn the hierarchy
let secret = try await config.fetchRequiredString(
forKey: "third_party.service.api_key",
context: ["version": 2],
as: ServiceSecret.self
)
// Secret provider will be used inn the hierarchy
let staticCredentials = try await sut.fetchRequiredString(
forKey: "server.database.credentials",
as: DatabaseCredentials.self
)
// This ConfigKey has not been registered in VaultSecretProvider,
// so the credentials will be read from the JSONProvider
let absoluteKey4 = AbsoluteConfigKey(["job", "database", "credentials"])
let databaseKey: String = absoluteKey4.components.joined(separator: ".")
var credentials = try await sut.fetchRequiredString(
forKey: databaseKey,
as: DatabaseCredentials.self
)
// Mutate the secret provider by adding a new mapping key and action
secretProvider.updateEvaluation(
absoluteKey4,
with: try await VaultSecretProvider.databaseCredentials(mount: databaseMount, role: .dynamic(name: dynamicRole))
)
// This time, the credential is retrieved from Vault,
// since the key is now registered and takes precedence in the hierarchy.
credentials = try await sut.fetchRequiredString(
forKey: databaseKey,
as: DatabaseCredentials.self
)
Would love to hear others' experiences or design thoughts around similar REST-backed ConfigProvider implementations.