-
Notifications
You must be signed in to change notification settings - Fork 95
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
Implementating ReferenceResolver in managed reconciler #46
Conversation
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.
Dealing in interface{}
worries me a little, but I'm intrigued by this reflection based approach in general. A few comments on the implementation thus far.
2d16cfd
to
429b651
Compare
pkg/resource/reference_resolver.go
Outdated
func validateReferencesAreReady(getter LocalObjectGetter, referencers []AttributeReferencer) error { | ||
for _, referencer := range referencers { | ||
for _, reference := range getAllObjectReferences(referencer) { | ||
var obj runtime.Object |
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.
Are you sure this works?
If I follow correctly getter
is effectively always r.reader.Get
. That method needs to know the type metadata (i.e. the group, version and kind) of the resource it is asked to get - you can't look an object up in the Kubernetes API by namespace and name alone. Get
determines the type metadata by using reflection to determine the name of the type contained within the runtime.Object
interface and looking that type up in its scheme (which is pretty much a big map of type names to pointers to instances of those types). Therefore I wouldn't expect passing a nil interface to Get
to work.
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.
You are right, it seems that the GKV is needed to retrieve an object. Do you think the following solution would make sense or would it be too noisy?
Add another method called RefereneKind
to the AttributeReference
, to make it look like:
type AttributeReferencer interface {
// ReferenceKind returns an object that represents an instance of the referenced object
ReferenceKind() runtime.Object
// Build retrieves and builds the referenced attribute
Build(getter LocalObjectGetter) (value string, err error)
// Assign accepts a resource object, and assigns the given value to the corresponding property
Assign(mg Managed, value string) error
}
and then populating ReferenceKind like
func (s *Subnet)ReferneceKind() runtime.Object{
return &VPC{}
}
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.
It seems weird that this level of details is needed to retrieve an object. According to the docs name and namespace should suffice to uniquely identify an object.
Namespaces provide a scope for names. Names of resources need to be unique within a namespace, but not across namespaces.
Maybe there is another api to retrieve an object given the name and namespace?
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.
It seems that a general Unstructured object also could be used. Digging more to see this still would need GKV.
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.
Another option would be to add a key to the resource:"attributereferencer kind=vpc.network.aws.crossplane.io/v1alpha2"
tag
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.
Let me see if I understand correctly. For the GCP subnetwork:
-
ValidateReady
would use the suppliedclient.Client
to validate that the GCP network was in stateReady
, returning an error if it was not. It would not validate the Provider (which doesn't have a concept of readiness). -
Build
would use the suppliedLocalObjectGetter
to get theNetwork
and itsProvider
, then return a string formed from the two, e.g./projects/id/global/networks/name
. -
Assign
would set thenetwork
field of the suppliedManaged
to the value returned byBuild
.
Am I interpreting this correctly? If so it seems like a good direction. My only comments would be:
- Do we need the
kind
struct tag in this design? It's not immediately obvious to me why. - There's a lot of similarity between
LocalObjectGetter
andclient.Client
(orclient.Reader
). CouldValidateReady
andBuild
take the same client-ish argument?
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.
Yes, this is exactly what I meant!
Do we need the kind struct tag in this design? It's not immediately obvious to me why.
No we don't. Sorry for the confusion, the comment that followed was meant for the case we are going the path of using tags.
There's a lot of similarity between LocalObjectGetter and client.Client (or client.Reader). Could ValidateReady and Build take the same client-ish argument?
Yes, I was exactly thinking the same, both signatures need to be equal.
The rational behind passing LocalObjectGetter
is two things:
- validate only local references are being made (by only accepting such type as input)
- hide details like
namespace
andcontext.Context
However, looks like in the new design we do need to read objects possibly from other namespaces (provider
in Subnetwork
case). To solve this problem, I am going to pass a client.Reader
and a context.Context
, but in reflection step require that the only field in the implementer type is an embedded LocalObjectReference
.
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.
This sounds good to me! Thanks for bearing with me on this design.
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.
ValidateReady
would use the suppliedclient.Client
to validate that the GCP network was in stateReady
, returning an error if it was not. It would not validate the Provider (which doesn't have a concept of readiness).Build
would use the suppliedLocalObjectGetter
to get theNetwork
and itsProvider
, then return a string formed from the two, e.g./projects/id/global/networks/name
.Assign
would set thenetwork
field of the suppliedManaged
to the value returned byBuild
.
If I have 5 reference fields, I'd have to implement 15 different methods, all very similar to each other. My two cents, I feel like one Resolve
method to do all this for one field is simpler, which would result in 5 methods.
Maybe, for simple fields that gets a value from the referred resource and assigns it to another field in the referrer, we might even be able to have a default implementation like SimpleFieldResolver
that developer can just opt-in to use for specific fields. Not 100% sure if that's technically feasible though.
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.
@muvaf The subject of why breaking down the Resolve
method to smaller ones, came up earlier in this thread #46 (comment)
breaking it down to smaller methods so the user is enforced to roughly follow the pattern of
{ValidateReady,Build,Assign}
I understand that it implementing the AttributeReferencer
interface would be noisy, but I think this scaffolding is easier to enforce at the time of development, and more useful in the managed resource (things like determining the condition state of the resolution).
As for creating SimpleFieldResolver
, I agree. One example would be VPCIdResolver
in AWS types that would be roughly the same (except the assign implementation), that I am considering to use, so all types that need to refer to a vpcId use that type and avoid defining their own.
9398cbe
to
c159a11
Compare
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 am not sure of the value in us providing a default resolver, especially given the high complexity of (r *APIManagedReferenceResolver) ResolveReferences(ctx context.Context, mg Managed)
that is necessary to capture many scenarios. First use of struct tag, panic possiblity, use of heavy reflection, iterating over fields of a struct; things we seem to be forced to do.
On the other hand, the difference in my responsibility as the stack implementor doesn't change as much when using the default resolver. When I read the one-pager, I imagined I needed to implement a function for a reference field similar to https://github.com/crossplaneio/crossplane-runtime/pull/46/files#r334722588 or an implementation with Build
& Assign
methods. Using the new ManagedReferenceResolver
interface, implementor could provide a ResolverChain []ManagedReferenceResolver
and we can call that chain just like Initializer
and Finalizer
chains. If any of them returns error, we don't proceed.
But I see that we are taking on the responsibility of discovering the fields that satisfies the needed interface by going through each field via reflection and call their resolve functions. Also, in the resulting picture we lose some type information that could actually benefit implementation. However, as stack implementor, there is not much difference for me. The dependency logic will be implemented by me in both cases but with the current structure I just don't have to put my resolvers in SetupWithManager
but expect crossplane-runtime to discover them. I think I can't see how this trade-off makes sense.
pkg/resource/managed_reconciler.go
Outdated
if err := r.managed.ResolveReferences(ctx, managed); err != nil { | ||
// update the status according to the type of the err | ||
switch err.(type) { | ||
case NotReadyError: |
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.
NotReadyError
is implemented by 82 structs according to Goland IDE. I think it's not that specific of an interface for us to make its implementation a differentiation factor.
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.
Good to know. Are you suggesting to use a different name?
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.
It's actually because of too much similarity with error
type. If NotReadyError
interface has a method other than Error() string
, that'd be sufficiently different I think. It'd still satisfy Error
interface and others wouldn't satisfy NotReadyError
since they don't know about that other method; maybe a method to get list of unresolved dependencies? #46 (comment)
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.
@muvaf I have changed the NotReadyError
in the later commits, let me know what you think
pkg/resource/managed_reconciler.go
Outdated
if !managed.IsConditionTrue(v1alpha1.TypeReferencesResolved) { | ||
if err := r.managed.ResolveReferences(ctx, managed); err != nil { | ||
// update the status according to the type of the err | ||
switch err.(type) { |
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.
If dependency graph is not yet resolved, should we consider this as error? I mean it's an expected path of execution, not something that shows a fault in either input or output. IMO, having ResolveReferences
somehow returning an array of unresolved references could make more sense. Error would be nil
but if the array is not empty, we'd say resolve blocked and possibly list the kind/name
of the resources that are blocking.
pkg/resource/reference_resolver.go
Outdated
func validateReferencesAreReady(getter LocalObjectGetter, referencers []AttributeReferencer) error { | ||
for _, referencer := range referencers { | ||
for _, reference := range getAllObjectReferences(referencer) { | ||
var obj runtime.Object |
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.
ValidateReady
would use the suppliedclient.Client
to validate that the GCP network was in stateReady
, returning an error if it was not. It would not validate the Provider (which doesn't have a concept of readiness).Build
would use the suppliedLocalObjectGetter
to get theNetwork
and itsProvider
, then return a string formed from the two, e.g./projects/id/global/networks/name
.Assign
would set thenetwork
field of the suppliedManaged
to the value returned byBuild
.
If I have 5 reference fields, I'd have to implement 15 different methods, all very similar to each other. My two cents, I feel like one Resolve
method to do all this for one field is simpler, which would result in 5 methods.
Maybe, for simple fields that gets a value from the referred resource and assigns it to another field in the referrer, we might even be able to have a default implementation like SimpleFieldResolver
that developer can just opt-in to use for specific fields. Not 100% sure if that's technically feasible though.
@muvaf thanks for the review! I think my main goal here was to make the cross referencing as managed as possible, by laying out the overall required steps, and detecting the references automatically, rather than leaving it to the developer to write the logic and then just simply execute it by putting it in
That said, I agree that there is a trade-off here. It would be possible to go the |
8dacf45
to
34ceaf8
Compare
crossplane#46 crossplane/crossplane-tools#8 This is introduced in the above runtime PR, but I merged the above tools PR too early and now `make generate` will use angryjet to generate methods that assume this underlying method exists when it does not. Signed-off-by: Nic Cope <negz@rk0n.org>
pkg/resource/reference_resolver.go
Outdated
// ResolveReferences resolves references made to other managed resources | ||
func (r *ReferenceResolver) ResolveReferences(ctx context.Context, mgd Managed) (err error) { | ||
// this recovers from potential panics during execution of | ||
// AttributeReferencer methods |
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.
Per discussion this morning I'd prefer us to remove this. It may be a good direction, but it would apply to all the places we execute implementations of interfaces that are supplied in stacks so I think we should defer for now and add this in general later if we think it's useful.
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.
This LGTM, pending the topics we discussed this morning. Specifically:
- Removing class level reference resolution if we can get away with it
- Removing the panic recovery logic
- Removing the superfluous IsConditionReady method.
All other comments (except the two missing tests) I've left should be considered non-blocking. I'd be happy to merge this PR without resolving them.
Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 <javad@upbound.io>
…ace, to show more granular information about the status of the referencers Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 <javad@upbound.io>
…d of Managed Signed-off-by: soorena776 <javad@upbound.io>
e64e4a9
to
701731d
Compare
- Removing the panic recovery logic - Removing the superfluous IsConditionReady method - Adding Nic's unit-tests for GetCondition and IsConditionTrue - Minor other fixes Signed-off-by: soorena776 <javad@upbound.io>
Signed-off-by: soorena776 javad@upbound.io
Description of your changes
Fixes crossplane/crossplane#887
Checklist
I have:
make reviewable
to ensure this PR is ready for review.clusterrole.yaml
to include any new types.