-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement a new standalone client #215
Conversation
The see the generated APIs you need to checkout the PR and at least run |
59014c1
to
267af37
Compare
@vsevel The client now implements all the The apis follow the documentation pretty close and methods are named after the documented title (e.g. I'm interested to see what changes are suggested for generated code before this is merged and the apis become somewhat "locked". Here's the complete list of supported apis:
|
@vsevel We should probably do a beta release (e.g |
3a976e2
to
1786658
Compare
@kdubb How does this change affect the backward compatibility of the extension API? Will I have to update my code that uses the extension after updating the extension to the version with the new client? |
@aaronz-vipaso Currently this PR is purely additive. It doesn't touch the extension in any fashion. When integrating it we can choose to re-implement the current "engines" with the new client or we can choose to remove them and have people use the new client instead. Personally, I'm in favor or doing a major release (e.g. 4.0) and removing the current engine implementations and having the extension use the new client to implement Quarkus specific features like static configuration, configuration sourcing, and dynamic credentials. For most users this will not be a source breaking change as the most common use seems to be to provide configuration values and/or dynamic credentials, not to interact directly with Vault via APIs. For those of us interacting with Vault directly, the current engines are very limited and statically configured (except those that have been updated to allow dynamic mounting). Switching to the new client should only be a positive change and should be fairly simple. For example, previously you might inject the @Inject VaultPKIManagerFactory pkiFactory;
void issueCertFromMount(String pkiMount) {
var pki = pkiFactory.engine(pkiMount);
pki.issue(...);
}
// or using the Quarkus "configured" PKI mount
@Inject VaultPKIManager pki;
void issueCert() {
pki.issue(...);
} With the new client the code would look like: @Inject VaultClient client;
void issueCertFromMount(String pkiMount) {
client.secrets().pki(pkiMount).issue(...);
}
// or using a configured mount
@ConfigProperty("app.pki.mount") String pkiMount;
@Inject VaultCleint client;
void issueCert() {
client.secrets().pki(pkiMount).issue(...);
} While this will be a source breaking change, having multiple methods to do something is a maintenance nightmare and the new client is simpler to use for these tasks, has a uniform api, and naturally supports dynamic usage. |
55b2972
to
fc24917
Compare
@aaronz-vipaso I've completed this PR by replacing the extension's client with the standalone client, leaving in place all the current engines. It does remove all the "internal" classes, but as long as your code uses the public classes it shouldn't require any changes. |
@vsevel My suggested plan for this is as follows.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future we can look at generating the YAML files from some other source like scraping the web docs or if the OpenAPI schema starts to fill in the gaps.
are they not using a similar approach?
can't we use the same source?
client/src/main/java/io/quarkus/vault/client/api/common/VaultAnyResult.java
Show resolved
Hide resolved
client/src/main/java/io/quarkus/vault/client/common/VaultBinaryResult.java
Show resolved
Hide resolved
client/src/main/java/io/quarkus/vault/client/common/VaultJSONResultExtractor.java
Outdated
Show resolved
Hide resolved
client/src/main/java/io/quarkus/vault/client/common/VaultTracingExecutor.java
Outdated
Show resolved
Hide resolved
import io.quarkus.vault.client.json.JsonMapping; | ||
import io.smallrye.mutiny.Uni; | ||
|
||
public abstract class VaultHttpClient implements VaultRequestExecutor, AutoCloseable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a VaultHttpClient
is not a VaultClient
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. VaultRequestExecutor
is the central type. VaultClient
is just a member of the request processing chain; the root of it.
There are a few reasons why this is but it ensures a VaultClient
can't be anything but the root because it does special things (e.g. token unwrapping) that need access to a request executor which cannot be a VaultClient
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would there be a different name, avoiding the confusion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you are suggesting we rename. Currently (I believe) they are named for exactly what they do. Examples?
client/src/main/java/io/quarkus/vault/client/VaultClientConfiguration.java
Outdated
Show resolved
Hide resolved
client/src/main/java/io/quarkus/vault/client/http/jdk/JDKVaultHttpClient.java
Show resolved
Hide resolved
@@ -27,7 +27,7 @@ | |||
|
|||
import io.quarkus.test.QuarkusUnitTest; | |||
import io.quarkus.test.common.QuarkusTestResource; | |||
import io.quarkus.vault.runtime.client.VaultClientException; | |||
import io.quarkus.vault.client.VaultClientException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be an api change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I couldn't get around this without duplicating the name. Obviously, we need VaultClientException
to be in the client module. The only solution I could think of was to derive a copy of in the runtime but then I realized it would never be thrown by the client itself.
When I was thinking would be 2 PR's (one for adding the standalone client and one for replacing the in-extension client) we could do a 3.5 and then 4.0 release to match "breaking" changes. Currently it looks like we need to release a 4.0 client and have end-users deal with the minor changes.
generator/src/main/java/io/quarkus/vault/generator/errors/DisallowedParameterError.java
Outdated
Show resolved
Hide resolved
I look at all the current methods in the Vault ecosystem for getting a good source for the api request/response types and always found two problems...
|
one approach could have been to complement their specification. you do not repeat everything, you just extend their specification. just like SB does for kotlin, adding metadata when needed. |
My thought after looking at everything was a one-shot copy to start you off (it generates a basic YAML in spec format) and the we customize it. This would make re-running it for updates problematic but it seemed like the way to go to ensure we don't end up with a non-native feeling Java client. |
5144bb1
to
4997b47
Compare
why did you have to move |
The client couldn't depend on a Quarkus package to be standalone. |
Removes `VaultIOException` in favor of more informative `VaultClientException`. Also ensures all exceptions thrown by the client are `VaultClientException` instances. `VaultException` is retained for other uses, e.g. exceptions thrown/derived in the Vault extension.
Some outside uses of Jackson seem to miss then JsonValue/JsonCreator format for enumerations. Doubling up ensures easy access for users (using `getValue()` and `from`) as well as all uses with Jackson.
This does not currently support the “non-proxy hosts” configuration that the Vert.x client does
Wraps the default `ProxySelector` to support non-proxy hosts configuration.
15a0238
to
ef726a8
Compare
@vsevel Just FYI, I removed all the references to Java version in the POMs; mostly because I had setup the client to be 17 but target 11 before the changeover. It now relies on the parent quarkiverse POM for this. I verified in the logs that it compiles with Java 17. |
TL;DR
This implements a new standalone client as contemplated in #214. It aims to solve as many of the raised issues as possible while making the development easier as things move forward.
To see how it's used and some basic capabilities look at the very limited tests. There's some simple but good examples of its usage.
But... here's the headline...
Below is an overview of the implementation as it currently stands.
VaultRequest
&VaultReqeuestExecutor
The client is built around a
VaultRequest
. It is a class that models every feature of a Vault request like authentication tokens, namespaces, mounts, paths, etc; it ignores HTTP although there is some similarity due to the way Vault defines it's API.VaultRequest
s are processed by a simple single function interface theVaultRequestExecutor
. The interface is simple to implement and allows layering and progressively configuring the request as it's passed to the final executor, which will pass it to a Vault instance via an HTTP client implementation.This completely decouples all of the client from any specific HTTP implementation. Currently there are Vert.x and JDK implementations which are fairly small. I plan on breaking the Vertx client out into its own separate module. In the future modules for other HTTP clients can be added as requested (e.g. the Apache HttpClient).
This allows neat things like simplified mocking or adding a tracing logger to the chain.
The classes are located in
io.quarkus.vault.common
.Code Generation
I wrote a code generator using JavaPoet that reads from a YAML "spec" file. It's an all original YAML schema and associated generator designed to make it to generate Vault style APIs.
Vault does self report an OpenAPI schema but it is woefully underspecified and seemingly leaves properties un-types. Reading the documentation you get a much more complete picture of the apis. This is the reason for this dedicated generator. The positives are that it generates classes in the same style as we currently implement by hand and it is very easy to look at the Vault docs and transcribe a new API from them.
In the future we can look at generating the YAML files from some other source like scraping the web docs or if the OpenAPI schema starts to fill in the gaps.
I'll spare you the details in this PR of the YAML schema. I intend to write a document that fully explains it, until then you can see the specs in
src/main/specs
of the client module. Check outsecerets/kv1.yaml
!Generated APIs
For Each API spec two main classes are generated, a "Request Factory" where each method returns a partially configured
VaultRequest
and matching API where where each method returns an actual result from calling the API.This decoupling comes in very handy when implementing complicated authentication like using a wrapping token to retrieve a password to use in "auth/userpass" authentication.
As an example for the KV1 secrets engine an API class named
VaultSecretsKV1
is generated and a factory class namedVaultSecretsKV1RequestFactory
is generated.Each method in both these interfaces is named for the vault endpoint and takes the parameters appropriate to the method; and possibly a generated options class for methods with a lot of parameters.
These classes are vended by the vault client and it's "accessor" classes that allow user code to mimic the Vault API and docs.
See
VaultSecretsKV1
andVaultSecretsKV1RequestFactory
.Authentication via Token Providers
Authentication methods are implemented via
VaultTokenProvider
which simply vends a (possibly time limited)VaultToken
. Again these can be layered, for example token caching is implemented inVaultCachingTokenProvider
and simply wraps anotherVaultTokenProvider
.If the implementation needs to support unwrapping, it can be written to use a
VaultUnwrappedTokenProvider
for the unwrapped piece.Importantly, both
VaultTokenProvider
andVaultUnwrappedTokenProvider
are passed aVaultRequestExecutor
(viaVaultAuthRequest
) so they can execute Vault requests as needed.Altogether, this allows the token providers, using the generated request factories, to build and execute the required requests without needing a "client" or event the generated API object itself.
For example in the
VaultUserPassTokenProvider
here's how it calls Vault to login: