Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Conversation

@vmuzikar
Copy link
Contributor

@vmuzikar vmuzikar commented Dec 6, 2025

Closes #43290

Couple of notes:

  • This PR both adds polymorphism (to handle different types) as well as some refinements to the OIDC Client rep based on the previous feedback.
    • We could split this into separate PRs but seemed more natural to keep it in a single one as the polymorphism required some changes to the mapping logic.
  • Mapstruct was removed for the following reasons:
    • I didn't find a way for it to generate mappers with base representations that would be overridden in child mappers, i.e. generate a specific OIDCClientModelMapper with generic methods like toModel(BaseClientRepresentation rep) that would in fact map specific OIDCClientRepresentation.
    • Mapstruct excels in use cases that don't require too many custom and explicit mappings. This is not the case here, see the WIP version with more explicit mappings for comparison.
    • It implicitly maps based on property names and types by default, this can lead some unintentional mappings and is to very clear when mixed with the explicit mappings via @Mapping.
  • To support the polymorphism in OpenAPI spec, some custom logic needed to be added to the filter.
    • It basically does 2 things for both request payloads, and responses.
      • It replaces the generic base representations with specific ones listed in oneOf.
      • It adds the discriminator field if it's missing from the schema. This might happen when the discriminator field is handled only on the Jackson level and not part of the representation itself.
    • The source of truth is Jackson annotations.
    • SmallRye/Quarkus plans to add a native support for this in the future:
    • The biggest caveat here is testability. Before we actually try and consume the spec with e.g. some generator, we don't know if it's correct.

Follow-ups:

@vmuzikar vmuzikar force-pushed the client-v2-representations-2 branch 3 times, most recently from f2a27e2 to 437966d Compare December 8, 2025 07:58
@mabartos
Copy link
Contributor

mabartos commented Dec 9, 2025

@vmuzikar Thanks for the PR! Spotless is failing.

@mabartos
Copy link
Contributor

mabartos commented Dec 9, 2025

@vmuzikar Could you please provide some PR description of provided changes + caveats + what's necessary to complete to mark it as ready to review? Thanks

Copy link
Contributor

@mabartos mabartos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a brief look.

Comment on lines +30 to +34
Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions);

Stream<ClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions);
Stream<BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions);

Stream<ClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions);
Stream<BaseClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions);
Stream<ClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions);
Stream<BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions);
Stream<ClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions);
Stream<BaseClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions);
Optional<? extends BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions);
Stream<? extends BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions);
Stream<? extends BaseClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions);

Wouldn't it be better to use the wildcard of the type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to do that on the REST layer too, I believe. And there we don't want it, IMHO.

@vmuzikar vmuzikar force-pushed the client-v2-representations-2 branch from 437966d to d0d6d65 Compare December 10, 2025 16:16
@shawkins
Copy link
Contributor

I started on the operator controller logic again based upon this pr - a couple of additional thoughts:

  • specialization via subclassing will require separate controllers for each type. I don't think this will be too much of an issue as the fabric8 client scales much better now in terms of the number of connections / watches it can run concurrently. However it does complicate the logic a little, and allows for name conflicts in the Kubernetes resources (without webhooks, you can have client type x and y with the same name).

  • Related to this is that top-level subclassing isn't the only way to address this. Rather than protocol being a string, it could be a complex type:

clientId: foo
protocol: 
 {name: saml}

So you would still have a oneOf stucture for the supported protocols, but you'd only need a single controller for all client types.

The only big reason not to do this is if we think that the client subclass somehow changes the behavior of base client fields (even removing them).

Also the CRD generator would not understand this directly, but we of course don't have to fully rely on that for generation in the first iteration and we'll eventually have fabric8io/kubernetes-client#7220 to make whatever manipulations we need.

@vmuzikar vmuzikar force-pushed the client-v2-representations-2 branch from 7a11367 to 4f9354c Compare December 11, 2025 15:19
@vmuzikar
Copy link
Contributor Author

Added some notes to the desc, fixed spotless. This should be now ready for full review. Thanks.

@vmuzikar vmuzikar marked this pull request as ready for review December 11, 2025 15:23
@vmuzikar vmuzikar requested review from a team as code owners December 11, 2025 15:23
@vmuzikar
Copy link
Contributor Author

However it does complicate the logic a little, and allows for name conflicts in the Kubernetes resources (without webhooks, you can have client type x and y with the same name).

I think that's fine if we document it properly and add some clear error handling.

So you would still have a oneOf stucture for the supported protocols, but you'd only need a single controller for all client types.

Does it mean we'd have a single Client CR?

@shawkins
Copy link
Contributor

shawkins commented Dec 11, 2025

Does it mean we'd have a single Client CR?

Yes it does.

The other way to do this without a structural schema, and which is supported by the CRD Generator, is to add each possible protocol as sibling fields. Like ValueOrSecret, or EnvFromSource, etc. The controller then validates that only 1 is populated.

EDIT: it is possible to have a single CR even with using the full subclassing. You can have the top-level of the CR spec as the structure part of the schema (or the representations as siblings). It's just not as typical of an approach.

@vmuzikar vmuzikar force-pushed the client-v2-representations-2 branch from 4f9354c to 7717db7 Compare December 11, 2025 17:12
@vmuzikar
Copy link
Contributor Author

@shawkins IMHO separate CRs are a better approach than a single one. It's better UX for the users that they see only fields relevant to given client type. If the number of types gets out of our hands, we can always introduce a different approach l ike configmaps instead of CRs.

@shawkins
Copy link
Contributor

IMHO separate CRs are a better approach than a single one. It's better UX for the users that they see only fields relevant to given client type.

After thinking more about the controller implementation, I'd prefer a single CRD. I could confirm in the openshift console how it presents structural schema in the editor if you are open to considering it.

@vmuzikar
Copy link
Contributor Author

I could confirm in the openshift console how it presents structural schema in the editor

Ok, let's explore that.

@shawkins
Copy link
Contributor

shawkins commented Dec 12, 2025

I could confirm in the openshift console how it presents structural schema in the editor

Ok, let's explore that.

After looking more into the structural schema capabilities, I'd say they are still limited.

The validation contructs anyOf, oneOf, etc. can only references properties, and those properties must be already defined in the schema. There is also no support the concept of a descriminator.

The above prevents us from doing something that looks like this:

clientId: foo
protocol: 
 {name: saml}

Instead you still have to use sibling properties:

clientId: foo
...
saml: {}

Even saml would probably still be typed as object to allow for future field specialization - but initially it would be ackward specifying it as an empty object.

The openshift editor of course won't prevent you from entering both siblings. If you try to save it with both populated, you'll get a message like: Error "Invalid value: "": "spec" must validate one and only one schema (oneOf). Found 2 valid alternatives" for field "<nil>" - which isn't exactly helpful.

So either I'm missing something obvious or the facilties for doing this still aren't that great.

@vmuzikar
Copy link
Contributor Author

@shawkins Thanks for looking into this further. Sounds like the separate CRs is the way forward then?

@vmuzikar
Copy link
Contributor Author

@shawkins @mabartos @Pepo48 Wanted to ask for the review here.

@shawkins
Copy link
Contributor

@shawkins Thanks for looking into this further. Sounds like the separate CRs is the way forward then?

Yes, we'll have to create base controller logic and run a controller per type.

Copy link
Contributor

@shawkins shawkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just a couple of thoughts / nits.

I'm also not sure we need a model mapper provider yet as it's internal, not configurable, and has no alternative implementations - but it's not a deal breaker to leave in for now.

PathItem sortedPathItem = OASFactory.createPathItem();

// Add operations order: GET -> POST -> PUT -> PATCH -> DELETE -> HEAD -> OPTIONS -> TRACE
if (pathItem.getGET() != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit: null checks don't seem to be required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was introduced in some previous PR, I just moved things around. Seemed safer to check for nulls just in case there are some defaults for whatever reason in the newly created path item.

static void validateUnknownFields(ClientRepresentation rep) {
if (!rep.getAdditionalFields().isEmpty()) {
static void validateUnknownFields(BaseClientRepresentation rep) {
if (rep.getAdditionalFields().keySet().stream().anyMatch(k -> !k.equals(BaseClientRepresentation.DISCRIMINATOR_FIELD))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If having the protocol field not be part of the schema is causing it to be parsed into the additionalFields, perhaps we should just have it as a proper field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more inclined to keep as is, not to have arbitrary discriminator fields directly in the representation. We can (and most probably will) revisit as a follow-up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone uses these classes on the client side, they'll have to do something similar if they want to check for unknown fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, created #45047 so we can address it as a follow-up if you don't mind.

@vmuzikar
Copy link
Contributor Author

@shawkins Thank you for the review!

I'm also not sure we need a model mapper provider yet as it's internal, not configurable, and has no alternative implementations - but it's not a deal breaker to leave in for now.

The motivation was that I expected the client types to be extendible, so I wanted to implement it in the most generic way possible.

@vmuzikar vmuzikar force-pushed the client-v2-representations-2 branch from 7717db7 to ade9bd8 Compare December 19, 2025 15:03
@vmuzikar vmuzikar enabled auto-merge (squash) December 19, 2025 15:03
@vmuzikar vmuzikar disabled auto-merge January 6, 2026 15:23
@vmuzikar vmuzikar merged commit ed69f33 into keycloak:main Jan 6, 2026
86 checks passed
@vmuzikar vmuzikar deleted the client-v2-representations-2 branch January 6, 2026 15:23
shaidar pushed a commit to shaidar/keycloak that referenced this pull request Jan 6, 2026
…#44727)

* [admin-v2] Polymorphism, refined OIDC Client representation

Closes keycloak#43290

Signed-off-by: Václav Muzikář <[email protected]>

* Remove AbstractRepModelMapper

Signed-off-by: Václav Muzikář <[email protected]>

---------

Signed-off-by: Václav Muzikář <[email protected]>
Signed-off-by: sar <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Review and refine Client v2 representation

3 participants