Skip to content

add v2 apis#854

Merged
turkenh merged 7 commits intocrossplane:mainfrom
erhancagirici:ec-xpv2
Jul 22, 2025
Merged

add v2 apis#854
turkenh merged 7 commits intocrossplane:mainfrom
erhancagirici:ec-xpv2

Conversation

@erhancagirici
Copy link
Contributor

@erhancagirici erhancagirici commented Jul 8, 2025

Description of your changes

New V2 apis and removals to facilitate namespaced MRs.

v2 ManagedResourceSpec API:

  • writeConnectionSecretToRef drops namespace field and accepts local secret references.
  • providerConfigRef becomes typed and requires kind. apiGroup is intentionally left out, it is expected to be inferred by the provider code. Namespaced
  • deletionPolicy is removed. New resources should rely on managementPolicies to control external resource deletion.

External Secret Store support is removed
publishConnectionDetailsTo is removed from both cluster-scoped (legacy) and namespace-scoped (modern) MR specs.
ConnectionDetailsPublisherTo interface is removed.

Cross-namespace cross-resource references:
Introduced NamespacedResolutionRequest, NamespacedResolutionResponse, NamespacedMultiResolutionRequest, NamespacedMultiResolutionResponse types along with APINamespacedResolver . In the new types, references and selectors in the resolution requests can include optional namespace parameter. Otherwise, they will default to MR namespaces.

I have:

Need help with this checklist? See the cheat sheet.

Copy link
Member

@negz negz left a comment

Choose a reason for hiding this comment

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

This looks great overall - I like the direction. Thanks @erhancagirici!


package v2

import v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
Copy link
Member

Choose a reason for hiding this comment

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

Is there a way to avoid having the v2 package depend on v1 types?

Copy link
Contributor Author

@erhancagirici erhancagirici Jul 15, 2025

Choose a reason for hiding this comment

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

Technically yes, but I see two ways, both having its own drawback:
most of apis/common/v1 actually has many shared constructs not particularly "v1", like ManagementPolicies, Conditions, Selector, Reference types etc.

  • If we duplicate them, we end up with same but different structs, which needs superfluous handling in various places.
  • One option can be to refactor common stuff mentioned above to its own package, but that also kind of breaks existing imports in the ecosystem.
    Open to ideas here

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we could:

  • Move the shared stuff to apis/common/common.go
  • Have both common/v1 and common/v2 import common
  • Add type aliases in the v1 package to avoid breaking folks

I think I prefer that approach. I agree that v1 is a bit of a misnomer in the first place.

The other options I considered are:

  • Duplicate the types in both places. I like that this decouples the two APIs, but it forces us to have two implementations of any function that uses these types, which I think is a non-starter.
  • Duplicate the types in both places, also add internal versions, update our functions to work on the internal versions, and generate code to translate between v1/v2 and internal. This seems like the best approach in theory, but it's a ton of work for these little utility types.

Copy link
Member

Choose a reason for hiding this comment

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

We just had an offline sync with @erhancagirici on this and the above proposal sounds good in general. We discussed some details like linter complaining about common as a package name and whether we should put a deprecation notice on v1 aliased types/consts/funcs.

At the end, we feel good about suppressing linter on package name and going with common as suggested. apis/common is there since the begining and we're fine for it as a package name. Regarding the deprecation notice, we believe it would be good not to force consumers to immediately switch to the new place to minimize the required changes for now. So, if linter complains and forces consumers to migrate, then we should better not do it now (and track with an issue to do it later). This is something @erhancagirici will check and see.

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 have pushed the changes with the type aliasing approach.
One side note: I had to bump the controller-tools to 0.18.0 to consume following fixes about type aliases.

kubernetes-sigs/controller-tools#1061
kubernetes-sigs/controller-tools#1122
This bump transitively upgraded google.golang.org/protobuf to v1.36.5 which had some minor changes in the generated protobuf files (backward-compatible).

I haven't added the depreciation notices to aliased types in apis/common/v1 yet, as they cause large amount of linter warnings. I'll create a follow-up issue for this.

Comment on lines +183 to +194
// UnpublishConnection is no-op since PublishConnection only creates resources
// that will be garbage collected by Kubernetes when the managed resource is
// deleted.
func (a *APILocalSecretPublisher) UnpublishConnection(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) error {
return nil
}
Copy link
Member

Choose a reason for hiding this comment

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

iirc this'll always be a no-op now that external secret stores are gone.

No strong feelings about removing it now though - we could follow up later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let's have this as a follow-up

return false, errors.New(errSecretStoreDisabled)
// PublishConnection calls each ConnectionPublisher.PublishConnection serially. It returns the first error it
// encounters, if any.
func (pc LocalPublisherChain) PublishConnection(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) (bool, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we still need this with external secret stores gone? I think we'll always publish to one secret.

(Again not urgent to remove it though.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, this was needed for the WithLocalConnectionPublishers(p ...ConnectionPublisher) reconciler option, which is already irrelevant.

A question is, the original(legacy) one of becomes irrelevant too. Should we remove this one to make our intention clear? or keep it for the sake of not causing a code changes in the (native) providers for existing legacy resources?

func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ConnectionPublisher = PublisherChain(p)
}
}

Maybe make it no-op with deprecated comment?

Copy link
Member

Choose a reason for hiding this comment

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

I'd lean toward just removing it. Either way it's going to be work for upstream provider authors. If it's gone, it's more obvious they need to do that work.

I don't feel strongly though.

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'll remove this and PublisherChain and keep an unexported version for unit testing purposes for cases we need to mock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@erhancagirici erhancagirici force-pushed the ec-xpv2 branch 4 times, most recently from eb6e96e to 812d411 Compare July 15, 2025 18:50
@erhancagirici erhancagirici marked this pull request as ready for review July 15, 2025 18:54
@erhancagirici erhancagirici requested a review from a team as a code owner July 15, 2025 18:54
@erhancagirici erhancagirici requested review from negz and phisco July 15, 2025 18:54
Copy link
Member

@negz negz left a comment

Choose a reason for hiding this comment

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

Great work @erhancagirici! This is a smaller diff overall than I expected, which is good. None of my comments are blocking.

// is enabled.
// Deprecated: this is intended for LegacyManaged resources that had deletionPolicy
// ModernManaged resources should use NewManagementPoliciesResolver.
func NewLegacyManagementPoliciesResolver(managementPolicyEnabled bool, managementPolicy xpv1.ManagementPolicies, deletionPolicy xpv1.DeletionPolicy, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Some of these long var names are a bit redundant. Not blocking though, and I can see it matches the existing implementation.

return false, errors.New(errSecretStoreDisabled)
// PublishConnection calls each ConnectionPublisher.PublishConnection serially. It returns the first error it
// encounters, if any.
func (pc LocalPublisherChain) PublishConnection(ctx context.Context, o resource.LocalConnectionSecretOwner, c ConnectionDetails) (bool, error) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd lean toward just removing it. Either way it's going to be work for upstream provider authors. If it's gone, it's more obvious they need to do that work.

I don't feel strongly though.


var _ reconcile.Reconciler = &Reconciler{}

func TestModernReconciler(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

I do wonder whether there's diminishing returns in completely duplicating the test file. I imagine a lot of these subtest scenarios use the same code regardless of whether it's a legacy or a modern MR, right?

It seems like having two test files could be a bit of a pain - we'll need to add and update them both whenever we update the tests. On the other hand it does seem like the most defensive/conservative approach to make sure both legacy and modern MRs work with all parts of the reconciler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As you mentioned, this was mostly for being defensive. I tried to pinpoint the "common" tests ( relying only on the "base" managed functional), though ended up duplicating most already to cover some unvisited codepaths.
We might consider following up later with a refactored test setup, that runs the tests as a matrix for legacy vs modern maybe?

@negz
Copy link
Member

negz commented Jul 19, 2025

Officially deferring my reviewer powers to @turkenh if there's any other questions or concerns while I'm out on vacation. 😉

@erhancagirici erhancagirici force-pushed the ec-xpv2 branch 3 times, most recently from 8e150cd to b069fec Compare July 21, 2025 15:20
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
fixes deepcopy method generation for structs that has aliased types

Signed-off-by: Erhan Cagirici <erhan@upbound.io>
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
Signed-off-by: Erhan Cagirici <erhan@upbound.io>
- linters:
- revive
text: "var-naming: avoid meaningless package names"
path: apis/common
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@erhancagirici erhancagirici Jul 22, 2025

Choose a reason for hiding this comment

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

opted for this, because had to repeat this for all the new files, and nolint directives were appearing at the very top and potentially in godoc. not strongly opinionated though

Copy link
Member

@turkenh turkenh 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 to me, thanks @erhancagirici 🙌

ns = r.from.GetNamespace()
}

if err := r.client.List(ctx, req.To.List, client.MatchingLabels(req.Selector.MatchLabels), client.InNamespace(req.Namespace)); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Should this use ns instead of req.Namespace ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch, fixed

Signed-off-by: Erhan Cagirici <erhan@upbound.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants