feat(encryption): [11/N] KMS factory trait and catalog wiring#2650
Conversation
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks @xanderbailey! This reads well. The factory mirrors StorageFactory, the client is created exactly once in load() with errors propagated, and it is forwarded uniformly to every TableBuilder across all six catalogs. The kms_client(impl Into<Option<...>>) forwarding is a nice ergonomic, and the docs are clear. A few non-blocking questions, mostly about how this connects to the read path and to Comet.
| /// .load("my_catalog", props) | ||
| /// .await?; | ||
| /// ``` | ||
| fn with_kms_client_factory(self, kms_client_factory: Arc<dyn KmsClientFactory>) -> Self; |
There was a problem hiding this comment.
As [11/N] of a stack, wiring the catalog KMS client to the Table's EncryptionManager is the whole job here. The read path can't consume it until a later part connects the EncryptionManager to file reads (the FileKeyResolver seam from your prototype), so there's nothing to change here. The one thing worth keeping in mind for that later PR: the seam should be feedable by either this KmsClientFactory-built client (native KMS) or an external resolver (a Comet-style JVM key source), since Comet resolves keys JVM-side and would not set a KmsClientFactory. Flagging it now only so the two entry points converge on one EncryptionManager/resolver seam rather than forking.
| /// Called once during catalog initialization. Properties may include | ||
| /// KMS endpoint, region, credentials, or any backend-specific | ||
| /// configuration needed to construct the client. | ||
| async fn create_kms_client( |
There was a problem hiding this comment.
I first wondered if this should select the KMS per table, but the spec model is catalog-scoped KMS: per docs/docs/encryption.md the KMS is a catalog property (encryption.kms-type / encryption.kms-impl), and the per-table axis is the encryption.key-id master-key property, not a per-table KMS impl. So one client per catalog is right, and create_kms_client(&self.config.props) matches Java's KeyManagementClient.initialize(properties = catalog properties) exactly. No change on scope. The one thing to confirm is the other axis: that per-table encryption.key-id is honored when the EncryptionManager is built (the PR body implies it is). The recurring "key_metadata is implementation-specific" point (relevant to #2584/#2586, not this PR) is spec-backed too: format/spec.md field 131 and the encryption-keys section.
| /// use std::sync::Arc; | ||
| /// | ||
| /// let catalog = MyCatalogBuilder::default() | ||
| /// .with_kms_client_factory(Arc::new(MyKmsClientFactory)) |
There was a problem hiding this comment.
Adding this as a required method breaks external CatalogBuilder implementors. A default impl that ignored the factory would silently drop encryption config, which is worse, so requiring it is probably the right call. Just worth calling out the breakage explicitly (changelog) so downstream implementors are not surprised.
There was a problem hiding this comment.
Yeah I suspect this is desired
| master_key_size: AesKeySize, | ||
| } | ||
|
|
||
| impl MemoryKmsClientFactory { |
There was a problem hiding this comment.
MemoryKmsClientFactory is the natural test seam, but I do not see a test that a factory-configured catalog yields a table whose EncryptionManager actually holds the client. A small test (build catalog with_kms_client_factory(MemoryKmsClientFactory), load a table, assert the client reached the EncryptionManager) would lock the wiring in. Good fit for the loader test suite.
There was a problem hiding this comment.
Nice idea to have an end-to-end test for the in-memory catalog / kms. Added end-to-end test
03df8c2 to
5bda7fb
Compare
|
@blackmwk I think this is ready for review when you have a moment! Thanks in advance! |
blackmwk
left a comment
There was a problem hiding this comment.
Thanks @xanderbailey for this pr, just finised first round of review.
| /// Accepts either an `Arc<dyn KeyManagementClient>` or an | ||
| /// `Option<Arc<dyn KeyManagementClient>>`, so catalogs that hold an | ||
| /// optional client can forward it directly. Passing `None` is a no-op. | ||
| pub fn kms_client( |
There was a problem hiding this comment.
This change is odd to me, we could just skip this method call if it's None.
There was a problem hiding this comment.
Also it makes the api inconsistent with other parts.
There was a problem hiding this comment.
I did it like this because else in all call sites you'd need:
if let Some(c) = &self.kms_client {
builder = builder.kms_client(c.clone());
}which doesn't read very nicely in my opinion.
Would just Option<...> be better rather than the Into?
Seems like we get away with this for other fields because they're actually always present when we use this builder but KMS client is legitimately optional.
There was a problem hiding this comment.
It sound reasonable to me. From a user's point, I accept both apis. But from a maintainer's point, I prefer the api without Option, since it's easier to maintain api consistency, otherwise we need to remember that for optional fields, we should accept Option, and other fields we don't.
There was a problem hiding this comment.
Sorry maybe I didn't quite follow, what's the recommendation here? You're happy with this API or should we change? To me it doesn't seem crazy for a genuinely optional parameter to have an optional builder.
There was a problem hiding this comment.
I mean I'm still in favor of removing the Option, since other apis in this crate adopt this approach, e.g. if a parameter is optional, user just don't call the function to set it. It would be easier to ensure api consistency in this repo.
There was a problem hiding this comment.
remove optional builder, let me know what you think.
blackmwk
left a comment
There was a problem hiding this comment.
Thanks @xanderbailey for this pr!
Which issue does this PR close?
What changes are included in this PR?
Adds
KmsClientFactoryto the catalog builder to allow a catalog to build one KMS client per catalog, this mirrors Java's implementation . Note in rust we don't have reflection so using Java'sencryption.kms-implisn't an option for us. This is a very similar pattern to what we do for theStorageFactoryso I believe the pattern is idiomatic for us.Are these changes tested?