Skip to content

Support for having circular dependencies while using referencers#328

Merged
muvaf merged 7 commits intocrossplane:masterfrom
sergenyalcin:fix-circular-reference
Jun 13, 2022
Merged

Support for having circular dependencies while using referencers#328
muvaf merged 7 commits intocrossplane:masterfrom
sergenyalcin:fix-circular-reference

Conversation

@sergenyalcin
Copy link
Copy Markdown
Member

@sergenyalcin sergenyalcin commented Apr 14, 2022

Description of your changes

Fixes #311
Fixes #250

This PR adds a new API about the reference resolution/resolve policies. The new Policy struct has two Policy types:

  • ResolvePolicy -> Always, IfNotPresent
  • ResolutionPolicy -> Required, Optional
vpcIdRef:
  name: my-vpc
  policy:
    # Resolve specifies when this reference should be resolved. The default
    # is 'IfNotPresent', which will attempt to resolve the reference only when
    # the corresponding field is not present. Use 'Always' to resolve the
    # reference on every reconcile.
    resolve: Always
    # Resolution specifies whether resolution of this reference is required.
    # The default is 'Required', which means the reconcile will fail if the
    # reference cannot be resolved. 'Optional' means this reference will be
    # a no-op if it cannot be resolved.
    resolution: Optional

This Policy API was added for both Reference and Selector structs. Also, to provide the Always resolution, the order of the resolving changed. Before first Reference then Selector, now first Selector then Reference.

For details please check the following comments:

#328 (comment)
#328 (comment)
#328 (comment)
#328 (comment)

I have:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable test to ensure this PR is ready for review.

How has this code been tested

For circular dependency fix, this PR was tested by provisioning the SecurityGroup resource in provider-aws (with the following manifest). This resource includes a circular dependency:

apiVersion: ec2.aws.crossplane.io/v1beta1
kind: SecurityGroup
metadata:
  name: sample-cluster-sg
spec:
  forProvider:
    region: us-east-1
    vpcIdRef:
      name: sample-vpc
    groupName: sample-ekscluster-sg
    description: Cluster communication with worker nodes
    ingress:
      - fromPort: 80
        toPort: 80
        ipProtocol: tcp
        userIdGroupPairs:
          - groupIdRef:
              name: sample-cluster-sg
              policy: 
                 resolution: Optional
        ipRanges:
          - cidrIp: 10.0.0.0/8
  providerConfigRef:
    name: example

For resolved only once issue, this PR was tested by provisioning the DBSubnetGroup resource with a problematic subnet reference list (with the following manifest):

apiVersion: database.aws.crossplane.io/v1beta1
kind: DBSubnetGroup
metadata:
  name: sergen-subnet-group
spec:
  forProvider:
    region: us-east-1
    description: "sample group"
    subnetIdRefs:
      - name: sergen-subnet
  providerConfigRef:
    name: example

Then this resource was updated with the following subnet reference list:

    subnetIdRefs:
      - name: sergen-subnet
         policy:
          resolve: Always
      - name: sergen-subnet2
        policy:
          resolve: Always

After adding the Always policy, I observed that the references were resolved for every reconcile loop.

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
@sergenyalcin sergenyalcin self-assigned this Apr 14, 2022
Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
@sergenyalcin sergenyalcin force-pushed the fix-circular-reference branch from 054d4e0 to ec82fef Compare April 16, 2022 13:04
Comment thread apis/common/v1/resource.go Outdated
Comment thread apis/common/v1/resource.go Outdated
)

// A Reference to a named object.
type Reference struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The policies here affect ref -> value transition but there is also selector -> ref where similar policies could apply. But as far as I can tell, both issues are about ref -> value, so having the policies only here could make sense for now until users see a need to customize selector -> ref as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't for example #250 (comment) pertain to selectors? I could imagine the race @chlunde describes being a problem here, i.e.:

  1. Foo A is created.
  2. fooSelector selects Foo A and creates a reference.
  3. Foo B is created
  4. fooSelector would have selected Foo B, but doesn't because it already successfully selected its references.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thank you for bringing up this, it was a point that I thought while working on these issues. Implemented supports/API for the Reference field can also be provided for Selector field, as you said. However, at this point, I think a new discussion can start.

Let me share my thoughts via an example:

..
forProvider:
  vpcIdSelector:
    matchLabels:
      key: value
..

As you know, while we using the Selector field for resolution, we are filling the Reference field. So after we selected and resolved the reference by using the Selector, new state of the resource will be like that:

..
forProvider:
  vpcId: vpc-1111111111
  vpcIdRef:
    name: sample-vpc
  vpcIdSelector:
    matchLabels:
      key: value
..

Now, when we review the function (this is Resolve function), we are observing that, firstly the Reference field is checked and then if it is not empty, the resolution process is done by using the Reference field and we return early. In other words, if the Reference field is set, we do not process the Selector field. Therefore, after this point, even if I update the Selector field, it will have no effect. Because the Reference field is full and only that field will be processed in the next loops.

I think this is an independent point from the resolution/resolve policies. Because, at this point, issues such as which resolution method is more dominant and which one to choose for situations where both are set will come to the fore. Let me share an example to clarify my point. Imagine that a user tries to provision an external resource by using the following manifest:

..
forProvider:
  vpcIdRef:
    name: sample-vpc
  vpcIdSelector:
    matchLabels:
      key: value
..

For a resource that is being create by this manifest, we can say that the reference in the Reference field is dominant. So, the Selector field will not be used for resolution process. In fact, this is the same situation we encountered after selecting references and filling Reference field via Selector. And from this point on, the Selector field will not be considered again. Therefore, changes on this field, such as updating list or changing the policy, will not be affective.

As a result, I think providing support of new Policy API for Selector field makes sense. However, if I do not miss any point, it seems that we will have to do this with a solution of the point I tried to explain above.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@negz Yes, you're right. We need to address the selector as well.

As a result, I think providing support of new Policy API for Selector field makes sense. However, if I do not miss any point, it seems that we will have to do this with a solution of the point I tried to explain above.

@sergenyalcin If I understood you correctly, you're saying that the fact that we don't take any action after the ref is found will be changed. I think that's fine since it's configured by the user.

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
Comment thread apis/common/v1/resource.go Outdated
@sergenyalcin
Copy link
Copy Markdown
Member Author

sergenyalcin commented May 1, 2022

About my comment, I want to propose a few solutions. Firstly, let me summarize my point again. When we set a Selector, we are selecting reference resources and putting them to the Reference field. In our Resolve functions, the Reference field is checking firstly, and after resolving references by using this Reference field we return early. This means that, Reference field is used as cache of Selector. So this blocks 'resolving always scenario' for Selector field (editing Selector and expecting some changes). Some proposed solutions:

  • Changing the order of Reference and Selector fields. By this way, the Selector field will be dominant and for every loop, if there is a change in Selector field, Reference field will be updated.
  • Now, we are writing the selected reference from Selector to Reference field. The another option can be that, differentiate the two APIS (Reference and Selector). I mean that, selected reference resource name from Selector cannot be written to the Reference field. By this way, we can protect the current logic in Resolve function.
  • We may consider that, do not return early after resolving the Reference field. We can let that resolving process can continue for the Selector field also. With this approach, both of the two fields will be tried to resolve for every loop. As a behavioral change of this approach, we will support the using two APIs together.

To be honest, I don't have a strong feeling about which of these would be the better approach. What are your thoughts @negz about this point? Do you have any other approach for addressing this? cc @muvaf

@muvaf
Copy link
Copy Markdown
Member

muvaf commented May 11, 2022

Changing the order of Reference and Selector fields. By this way, the Selector field will be dominant and for every loop, if there is a change in Selector field, Reference field will be updated.

I think this option is more intuitive from the perspective of users since the main job of selector is to select a reference and it makes it straight-forward to understand. If we add policy field to the selector as well, similar to reference, I think users could configure it while being aware of the risk that if a new resource with the same labels is added, then the list will change. I'd personally not opt for that risk but it's at least clear. In practice, I think it'll likely to be used temporarily during operations when a resource is stuck or wrong references are resolved and once it's done, the user would revert it back.

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
@sergenyalcin
Copy link
Copy Markdown
Member Author

sergenyalcin commented May 12, 2022

I think this option is more intuitive from the perspective of users since the main job of selector is to select a reference and it makes it straight-forward to understand. If we add policy field to the selector as well, similar to reference, I think users could configure it while being aware of the risk that if a new resource with the same labels is added, then the list will change. I'd personally not opt for that risk but it's at least clear. In practice, I think it'll likely to be used temporarily during operations when a resource is stuck or wrong references are resolved and once it's done, the user would revert it back.

@muvaf I added the new API for Selector and also changed the order of Reference and Selector fields. Now, we are resolving the Selector field firstly.

Comment thread apis/common/v1/resource.go Outdated
Comment thread apis/common/v1/resource.go Outdated
Comment thread apis/common/v1/resource.go Outdated
Comment thread pkg/reference/reference.go Outdated
Comment thread pkg/reference/reference.go Outdated
if err := r.client.Get(ctx, types.NamespacedName{Name: req.Reference.Name}, req.To.Managed); err != nil {
return ResolutionResponse{}, errors.Wrap(err, errGetManaged)
// The selector is set - resolve it.
if req.Selector != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This results in behavior change for cases where both ref and selector are given and no policy is set. Chatted offline about possible solutions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As we discussed, I preserved the order (first Reference field then Selector field).

For new Always policy scenario, I implemented a possible solution (we discussed offline) that sets the Reference field to nil when Selector is not empty and resolve policy is always.

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
@sergenyalcin
Copy link
Copy Markdown
Member Author

About this PR, I want ot share some points. We are trying to support new capabilities for the reference resoultion by providing policies. First one is the resolving references for every reconciliation loop (Always policy), the another one is that resolving references as Optional. We have some challenges about them, especially Always one.

I think, the existing API is very appropriate for the current behaviors:

  • Resolving references only once
  • Cannot be ignored the errors that occurred while reference. (it blocks the circular dependency)

Although this API works very successful for many cases, it seems that, for our new cases that we want support it has some weak parts. All the concerns that I will talk about a little later have been expressed from different angles in the issues. My purpose here is to write about these concerns in this PR and explore whether there is a better way.

I want to share two points (of course, we can talk about many difference cases). These two cases are about the new Always policy.

  • Some resources support multiple references like DBSubnetGroup (AWS). DBSubnetGroup can refer more than one Subnet resources. With this PR, we are providing a new API field for every Reference: Policy. So for every Subnet reference in the DBSubnetGroup we can determine different policies. Let me share an example:
SubnetIDRefs:
- name: subnet-1
    policy:
      resolve: Always
- name: subnet-2
- name: subnet-3
- name: subnet-4
    policy:
      resolve: Always

As you can see, some inconsistent situations may occur when the new API is used. For now, when we see an Always policy for an element of list, we are resolving all references for every reconcile loop. In other words, it is sufficient for only one of the elements in the list have an Always policy for resolution in each loop. This might seem intuitively reasonable. However, it is theoretically possible to implement the opposite of this behavior. So it's actually a preference, and it might be reasonable to implement a behavior otherwise.

  • When we use the Selector field with the Always policy, a scenario can be possible with the new behavior.
vpcIdSelector:
  matchLabels:
    key: value
  policy:
    resolve: Always

After first reconciliation loop:

vpcIdReference:
- name: vpc-example
vpcIdSelector:
  matchLabels:
    key: value
  policy:
    resolve: Always

Then user edited the Reference field value by using kubectl:

vpcIdReference:
- name: MANUALLY-CHANGED-vpc-example
vpcIdSelector:
  matchLabels:
    key: value
  policy:
    resolve: Always

In next reconciliation loop, because of the Always policy, selector will resolve the reference again and override the changed desired state of this resource:

vpcIdReference:
- name: vpc-example
vpcIdSelector:
  matchLabels:
    key: value
  policy:
    resolve: Always

For the first state of the resource, the Reference field was empty so filling this via controller is the late-initialization concept. However, after changed this value manually, this means that the user gives a new desired state. So I am not sure that we must override this.

So, for now, the new Policies are make sense and resolve the issues. Also I think this is an improvement for the reference resolution pipeline. However, we may discuss re-designing this API for providing better concept for our new cases.

Copy link
Copy Markdown
Member

@muvaf muvaf left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you @sergenyalcin for driving such a core change! I have some nits but it's good to go, let me know when it's ready to merge.

Comment on lines +22 to 24
kerrors "k8s.io/apimachinery/pkg/api/errors"

"k8s.io/apimachinery/pkg/types"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"

goimports

isAlways := false
if rr.Selector != nil {
if rr.Selector.Policy.IsResolvePolicyAlways() {
rr.Reference = nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you leave a comment here about why we set it to nil?

@elohmrow
Copy link
Copy Markdown

elohmrow commented Jun 3, 2022

💯

@muvaf
Copy link
Copy Markdown
Member

muvaf commented Jun 3, 2022

As you can see, some inconsistent situations may occur when the new API is used. For now, when we see an Always policy for an element of list, we are resolving all references for every reconcile loop. In other words, it is sufficient for only one of the elements in the list have an Always policy for resolution in each loop.

@sergenyalcin Could you open an issue regarding this? I don't love that each element has its own policy but there isn't really a good way of adding a policy to the whole array without a breaking change. Maybe an annotation to point to the array field and say policy of this field is Always?

For the first state of the resource, the Reference field was empty so filling this via controller is the late-initialization concept. However, after changed this value manually, this means that the user gives a new desired state. So I am not sure that we must override this.

I think this is pretty much the core use case users want with Always policy. In an ideal case, because the reference is changed by a controller, it would appear under status but then that reference field itself is actually a parameter. So, it's really hard to get both to work. Though personally, I feel better that it's configurable and default is consistent and IMO both policies will be used seldom.

Comment thread apis/common/v1/resource.go
// the corresponding field is not present. Use 'Always' to resolve the
// reference on every reconcile.
// +optional
Resolve *ResolvePolicy `json:"resolve,omitempty"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can make this enum.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it make sense for it to be possible to explicitly specify IfNotPresent? Currently that's not possible because the enum validation only allows Always.

Copy link
Copy Markdown
Member

@muvaf muvaf Jun 15, 2022

Choose a reason for hiding this comment

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

Yes, I think enum should include all possibilities. @sergenyalcin if there isn't a specific reason, it'd be nice to open a PR to add it before the change propagates to providers.

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resource creation fails when having circular dependencies using referencers Referencers are resolved only once

4 participants