Skip to content

Dependent resource hierarchy #958

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

Closed
wants to merge 4 commits into from
Closed

Conversation

csviri
Copy link
Collaborator

@csviri csviri commented Feb 18, 2022

Featuring also ReadOnly, PollingReadOnly resources. Edit feature for Kubernetes

@csviri csviri changed the base branch from main to next February 18, 2022 16:14
@csviri csviri self-assigned this Feb 18, 2022
@metacosm
Copy link
Collaborator

This doesn't appear to address the read-only use-case for Kubernetes dependent resources.

Also, if we're going to have configuration for this, I'd rather do it via interfaces to express traits than making it flags in the configuration. Using traits, we would only add the methods when needed (and to implement the support for create/update you would need these methods anyway) rather than do it with sub-classes (having trait interfaces would bypass the need for sub-classes since the base classes wouldn't need to provide abstract/default implementations).

Another issue is that this implementation duplicates code in different branches of the hierarchy (notably getResource) making it more difficult to maintain. Moreover, what happens if someone uses KubernetesReadOnlyResource and overrides reconcile? It's possible and, while unlikely, this could cause difficult to trace errors… The SDK should strive to help users avoid coding mistakes.

Having trait interfaces would also help for more precise RBAC creation in extensions of the SDK that do so in a generic way instead of having to know about all the possible sub-classes which would lead to fragile code base and implementation leakage.

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

This doesn't appear to address the read-only use-case for Kubernetes dependent resources.

There is the KubernetesReadOnlyDependentResource:
https://github.com/java-operator-sdk/java-operator-sdk/pull/958/files#diff-3d696bac55047d47272489b0c39f3cc786d2f5ae358cb230c0fe933d495eb2c9
Just with this approach it becomes a very simple thin layer.

Also, if we're going to have configuration for this, I'd rather do it via interfaces to express traits than making it flags in the configuration. Using traits, we would only add the methods when needed (and to implement the support for create/update you would need these methods anyway) rather than do it with sub-classes (having trait interfaces would bypass the need for sub-classes since the base classes wouldn't need to provide abstract/default implementations).

The downside of this is that, it's not very explicit. So what is the behavior if a trait object is missing. Also not sure how do you mean it. The DependentResource would be still as the same as it is, and there will be a subclass that has these traits?
Also if we just have traits, the common behavior is shared, what could be limiting, see the case when the spec reference object name is changing, and other use cases we don't think of now. Having a very generic reconcile() method is basically a future proof.
I'm fine to experiment using traits objects as a special implementation of the current DependentResource abstraction btw.

Moreover, what happens if someone uses KubernetesReadOnlyResource and overrides reconcile? It's possible and, while unlikely, this could cause difficult to trace errors… The SDK should strive to help users avoid coding mistakes.

The point is that for different behaviors like the generic case or the read only behavior we already provide abstract implementations, if somebody overrides it should know what is doing. I guess this is generally problem with anything else, like overriding a trait object and braking the behavior.

Another issue is that this implementation duplicates code in different branches of the hierarchy (notably getResource) making it more difficult to maintain.

I don't understand this, where do you mean it? Could you put here links to the code pls.

@csviri csviri linked an issue Feb 21, 2022 that may be closed by this pull request
@metacosm
Copy link
Collaborator

metacosm commented Feb 21, 2022

This doesn't appear to address the read-only use-case for Kubernetes dependent resources.

There is the KubernetesReadOnlyDependentResource: https://github.com/java-operator-sdk/java-operator-sdk/pull/958/files#diff-3d696bac55047d47272489b0c39f3cc786d2f5ae358cb230c0fe933d495eb2c9 Just with this approach it becomes a very simple thin layer.

It is not read-only though: the update method will still be called unless you override match to always return true, which more ugly than returning empty, imo. Granted, in most cases, it won't do anything but if you mistakenly happen to override the method, then your resource is not read-only anymore. So the framework is simply relying on naming to enforce something instead of enforcing it in the code.

Also, if we're going to have configuration for this, I'd rather do it via interfaces to express traits than making it flags in the configuration. Using traits, we would only add the methods when needed (and to implement the support for create/update you would need these methods anyway) rather than do it with sub-classes (having trait interfaces would bypass the need for sub-classes since the base classes wouldn't need to provide abstract/default implementations).

The downside of this is that, it's not very explicit. So what is the behavior if a trait object is missing. Also not sure how do you mean it. The DependentResource would be still as the same as it is, and there will be a subclass that has these traits? Also if we just have traits, the common behavior is shared, what could be limiting, see the case when the spec reference object name is changing, and other use cases we don't think of now. Having a very generic reconcile() method is basically a future proof. I'm fine to experiment using traits objects as a special implementation of the current DependentResource abstraction btw.

There will only be one default implementation that covers most cases. If people have a case that isn't covered, they could still create their own DependentResource implementation completely. The point of the dependent resources implementation as it is, is not to cover all possible use cases (because it's just not possible), the point is to provide an easy to use feature that covers 80% of the use cases.

Having lots of implementations for special cases doesn't help because it makes things more complex to understand, to maintain and to use. And it still wouldn't cover all the use cases.

Moreover, what happens if someone uses KubernetesReadOnlyResource and overrides reconcile? It's possible and, while unlikely, this could cause difficult to trace errors… The SDK should strive to help users avoid coding mistakes.

The point is that for different behaviors like the generic case or the read only behavior we already provide abstract implementations, if somebody overrides it should know what is doing. I guess this is generally problem with anything else, like overriding a trait object and braking the behavior.

There should only be one default implementation that covers 80% of the cases in a nice way. Not 10 different implementations that cover 90% in an awkward and error-prone way. :)

Another issue is that this implementation duplicates code in different branches of the hierarchy (notably getResource) making it more difficult to maintain.

I don't understand this, where do you mean it? Could you put here links to the code pls.

https://github.com/java-operator-sdk/java-operator-sdk/pull/958/files#diff-3d696bac55047d47272489b0c39f3cc786d2f5ae358cb230c0fe933d495eb2c9R13

https://github.com/java-operator-sdk/java-operator-sdk/pull/958/files#diff-aa20588ab4b1ff4f171a897d4042d1b055c02737b067d09aaeb9cfd770adf3a0R136

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

It is not read-only though: the update method will still be called unless you override match to always return true, which more ugly than returning empty, imo. Granted, in most cases, it won't do anything but if you mistakenly happen to override the method, then your resource is not read-only anymore. So the framework is simply relying on naming to enforce something instead of enforcing it in the code.

Pls check it again, I think you misread it, it does not extend the KubernetesDependentResource, and has no access to any of the methods you mention.

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

There should only be one default implementation that covers 80% of the cases in a nice way. Not 10 different implementations that cover 90% in an awkward and error-prone way. :)

What use case we don't cover with this PR?

There is basically one base implementation that is extended with one generic. That's it, and if somebody want to implement it's own for external resources it is now much more easy and straight-forward now , see the Polling for read only. With traits what would be quite confusing to do.

For me again the whole point of the domain modelling is have these notions explicit, and not hack it with a seemingly simple way, where are the cases are not explicitly captured, so it's quite confusing. But again happy take a look with traits if you add a prototype.

@metacosm
Copy link
Collaborator

For me again the whole point of the domain modelling is have these notions explicit, and not hack it with a seemingly simple way, where are the cases are not explicitly captured, so it's quite confusing. But again happy take a look with traits if you add a prototype.

The prototype was the implementation that was written 3 months ago… :)

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

@metacosm
Copy link
Collaborator

metacosm commented Feb 21, 2022

It is not read-only though: the update method will still be called unless you override match to always return true, which more ugly than returning empty, imo. Granted, in most cases, it won't do anything but if you mistakenly happen to override the method, then your resource is not read-only anymore. So the framework is simply relying on naming to enforce something instead of enforcing it in the code.

Pls check it again, I think you misread it, it does not extend the KubernetesDependentResource, and has no access to any of the methods you mention.

Indeed. However I can still override reconcile or delete in read-only resources. The point I was trying to make still stands: what makes these resources read-only apart from their name?

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

So I guess I can't override reconcile or delete in read-only resources? What makes them read-only apart from their name?

you don't have delete there if you take a look. If somebody calls reconcile it won't do anything, you can just call getResource()

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

The prototype was the implementation that was written 3 months ago… :)

That was quite a different situation we did not have the reconcile() or the current DependentResource in there at all.
As I said, that is fine to have just, you did not answered the question, what case we don't cover now, with this simple class hierarchy.

@csviri csviri closed this Feb 21, 2022
@csviri csviri reopened this Feb 21, 2022
@metacosm
Copy link
Collaborator

metacosm commented Feb 21, 2022

https://github.com/java-operator-sdk/java-operator-sdk/pull/958/files#diff-3d696bac55047d47272489b0c39f3cc786d2f5ae358cb230c0fe933d495eb2c9R13
https://github.com/java-operator-sdk/java-operator-sdk/pull/958/files#diff-aa20588ab4b1ff4f171a897d4042d1b055c02737b067d09aaeb9cfd770adf3a0R136

fixed this. Moved to base class. But needs different case for generic see, other PR.

Since the class is now empty, why do we even need it? It doesn't enforce anything, doesn't provide any new feature, it simply increases the cognitive load on the user that needs to figure out why they would use this implementation as opposed to another one.

So I guess I can't override reconcile or delete in read-only resources? What makes them read-only apart from their name?

you don't have delete there if you take a look. If somebody calls reconcile it won't do anything, you can just call getResource()

ReadOnlyDependentResource is a DependentResource so it provides reconcile and delete, which can be overridden. The only thing it does is that it provides a default empty implementation. Again, nothing enforces the read-only nature of implementations of this interface. If your implementation overrides reconcile (which, I admit, would be pretty weird), your implementation is not read-only anymore. The SDK doesn't enforce that read-only nature in the code, only in convention.

More precisely, how is relying on empty implementation of methods any better than enforcing a behavior via a return type? For the latter, you can actually enforce it in code.

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

Since the class is now empty, why do we even need it? It doesn't enforce anything, doesn't provide any new feature, it simply increases the cognitive load on the user that needs to figure out why they would use this implementation as opposed to another one.

It implements the read only interface. And that is not desired for the generic one.

ReadOnlyDependentResource is a DependentResource so it provides reconcile and delete, which can be overridden. The only thing it does is that it provides a default empty implementation. Again, nothing enforces the read-only nature of implementations of this interface. If your implementation overrides reconcile (which, I admit, would be pretty weird), your implementation is not read-only anymore. The SDK doesn't enforce that read-only nature in the code, only in convention.
More precisely, how is relying on empty implementation of methods any better than enforcing a behavior via a return type? For the latter, you can actually enforce it in code.

You can override reocncile in any case, this is not an argument at all. If somebody want's to break it can. It's not about that, it's defining with an interface a common behavior abstraction.

The optional way woudl be pretty ugly on the other hand, since one would have to override delete and update and create. While those won't be used. Think about how something like PollingReadOnlyDependentResource would be implemented, would be super ugly to force it there.

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

@metacosm what you are refering is the open-closed principle that is a good argument for the traits (not for the optional IMHO).

And if we take a step back basically the originally why we moved from the traits, is that it is open to extension, but there is a problem how it describes the domain/use cases explicitly, and might be problems with generality, like to cover use cases which we cannot see, and even what we can see are there all trait combinations make sense? And things like that.

So again don't know how to put a dot at the end of this discussion. But having this class hierarchy make much more sense to me since, we can prepare quite nice abstractions and generic toolkit with a relatively simple class hierarchy. That users just can simply extend and implement custom dependent resources.

I'm up to experiment and move forward with traits also if it implements the DependentResource interface. Maybe having even both is not a sin eventually. Not that we need an abstraction like DependentResource to implement a workflow engine on top of it. But Optional for the desired seems like a unnecessary hack for me.

@metacosm
Copy link
Collaborator

Since the class is now empty, why do we even need it? It doesn't enforce anything, doesn't provide any new feature, it simply increases the cognitive load on the user that needs to figure out why they would use this implementation as opposed to another one.

It implements the read only interface. And that is not desired for the generic one.

Then just implement the interface. No need for an empty class that doesn't add anything useful.

ReadOnlyDependentResource is a DependentResource so it provides reconcile and delete, which can be overridden. The only thing it does is that it provides a default empty implementation. Again, nothing enforces the read-only nature of implementations of this interface. If your implementation overrides reconcile (which, I admit, would be pretty weird), your implementation is not read-only anymore. The SDK doesn't enforce that read-only nature in the code, only in convention.
More precisely, how is relying on empty implementation of methods any better than enforcing a behavior via a return type? For the latter, you can actually enforce it in code.

You can override reocncile in any case, this is not an argument at all. If somebody want's to break it can. It's not about that, it's defining with an interface a common behavior abstraction.

But that behavior doesn't exist. It's only materialised by a specific implementation and a naming convention. There's no "read-only" concept that you can enforce in the code of the SDK with this implementation.

The optional way woudl be pretty ugly on the other hand, since one would have to override delete and update and create. While those won't be used. Think about how something like PollingReadOnlyDependentResource would be implemented, would be super ugly to force it there.

There's no PollingDependentResource at the moment either, so you already need to implement all these methods and I would argue that there shouldn't be one such implementation anyway. Returning empty instead of something would not make things more difficult to implement, imo.

The value of the SDK should be to make the common use cases very easy to implement, define a pattern that can be extended to all implementations, provide support for that pattern in the code, instead of trying to exhaustively support every use case we can think of. Having a class hierarchy doesn't address the generality problem, it just keeps adding special cases that make the code base more confused, more difficult to maintain and harder to learn. It also forces extensions of the SDK to know about theses implementations instead of relying on a simpler, generic pattern thus making them more fragile with respect to changes in the implementation.

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

Then just implement the interface. No need for an empty class that doesn't add anything useful.

The problem with this, that the KubernetesDependentResource already would implement Generic and ReadOnly interface as the result. What is a bigger smell IMHO.

But that behavior doesn't exist. It's only materialised by a specific implementation and a naming convention. There's no "read-only" concept that you can enforce in the code of the SDK with this implementation.

There's no PollingDependentResource at the moment either, so you already need to implement all these methods and I would argue that there shouldn't be one such implementation anyway. Returning empty instead of something would not make things more difficult to implement, imo.

With this just wanted to show the extensibility. Or how easy is to do it. We don't have anything for this in previous PR. But the point is that we have to think about it eventually, and have a vision for it's design.

The value of the SDK should be to make the common use cases very easy to implement, define a pattern that can be extended to all implementations, provide support for that pattern in the code, instead of trying to exhaustively support every use case we can think of.

With this PR basically we do this, with a transparent way. So again don't understand why is having an Optional in desired makes this more clean, it makes the whole api very confusing. We just separating the concerns that should be separated, and as @scrocquesel mentioned not use fat classes, that does everything. In this way also is nicely extensible.

Having a class hierarchy doesn't address the generality problem, it just keeps adding special cases that make the code base more confused, more difficult to maintain and harder to learn. It also forces extensions of the SDK to know about theses implementations instead of relying on a simpler, generic pattern thus making them more fragile with respect to changes in the implementation.

IMHO We add special cases with the "optional desired", for an implementation that is already targets the generic use cases. That is hard to understand. Much harder than having a dedicated abstraction for a sub-problem.

# Conflicts:
#	operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
@sonarqubecloud
Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 19 Code Smells

27.0% 27.0% Coverage
0.5% 0.5% Duplication

@metacosm
Copy link
Collaborator

Then just implement the interface. No need for an empty class that doesn't add anything useful.

The problem with this, that the KubernetesDependentResource already would implement Generic and ReadOnly interface as the result. What is a bigger smell IMHO.

Extending an empty class is a bigger smell than implementing a marker interface because it forces you into a class hierarchy, which is a bad pattern for frameworks/SDK. If you think that's a smell, then that's a problem with the design. The bigger problem is whether you extend the implementation or implement the interface, the SDK still cannot enforce the read-only functionality, which is the bigger smell of all, imo. :)

IMHO We add special cases with the "optional desired", for an implementation that is already targets the generic use cases. That is hard to understand. Much harder then having a dedicated abstraction for a sub-problem.

It's very simple, conceptually, though: if, for any reason, your implementation doesn't know what to return at a specific time, then you return empty. As simple as that. This doesn't overload the concepts, it's very natural for an implementation to not have a desired state at some point because it might not be ready at this time, or because it never has a desired state at all. Returning Optional covers more cases than just the read-only case, without having to create 5 new classes just to add a behavior that is not even enforced in the SDK.

@csviri
Copy link
Collaborator Author

csviri commented Feb 21, 2022

the SDK still cannot enforce the read-only functionality, which is the bigger smell of all, imo.

Hmm, but how is possible to do that? I mean even with traits, or with the desired, it's basically you can always do that. So not sure how you mean it exactly.

It's very simple, conceptually, though: if, for any reason, your implementation doesn't know what to return at a specific time, then you return empty. As simple as that. This doesn't overload the concepts, it's very natural for an implementation to not have a desired state at some point because it might not be ready at this time, or because it never has a desired state at all. Returning Optional covers more cases than just the read-only case, without having to create 5 new classes just to add a behavior that is not even enforced in the SDK.

I understand what you mean, and I agree that it does not bring too much complexity in that regard. The problem is it breaks the abstraction. So the GenericDependentResource meant to describe the cases when the reconciliation looks exactly like this:

 var actual = getResource(primary);
    var desired = desired(primary, context);
    if (actual.isEmpty()) {
      create(desired, primary, context);
    } else {
      if (!match(actual.get(), desired, context)) {
        update(actual.get(), desired, primary, context);
      }
    }

What is the most common case, when an controller owns the resources, and reconciles it. Whit optinal desired, we cover that and some additional more exotic case, what could desire a completely different abstraction, since we don't just kubernetes but also other external resources that will implement that. In this case this is the read only. So I don't understand this part:
Why would we combine exactly these two use cases? So the only editable is not there, how that should there added?

So what I try to say, is the good abstractions of the problems/cases should have the highest priority. And here the generic and the read-only cases deserves it's own abstraction under dependent resource. At least for me this is much more important than having done the current scope (that will probably be extended) with some adjustments to abstractions that has well defined scopes.

This doesn't overload the concepts, it's very natural for an implementation to not have a desired state

It's not that natural, basically then it's not a dependent resource, it's an informer. Not sure if we should actually even solve this. I mean we have to just because we want to have it nicely done with annotations, without that actually we don't even need this. We could just reference the informer from the dependent resource what is using it as an input.

@metacosm
Copy link
Collaborator

metacosm commented Feb 21, 2022

the SDK still cannot enforce the read-only functionality, which is the bigger smell of all, imo.

Hmm, but how is possible to do that? I mean even with traits, or with the desired, it's basically you can always do that. So not sure how you mean it exactly.

If desired returns empty then you can enforce that you're in the not created/not updated case for this particular reconciliation (note that what is returned doesn't need to be constant, it can vary over time and depend on external conditions). For the read-only case, then desired would always return empty and the default implementation would enforce this read-only situation because update or create would never be called. Granted, this would only work in the "managed" case. If people are implementing reconcile directly, then, obviously, all bets are off.

It's very simple, conceptually, though: if, for any reason, your implementation doesn't know what to return at a specific time, then you return empty. As simple as that. This doesn't overload the concepts, it's very natural for an implementation to not have a desired state at some point because it might not be ready at this time, or because it never has a desired state at all. Returning Optional covers more cases than just the read-only case, without having to create 5 new classes just to add a behavior that is not even enforced in the SDK.

I understand what you mean, and I agree that it does not bring too much complexity in that regard. The problem is it breaks the abstraction. So the GenericDependentResource meant to describe the cases when the reconciliation looks exactly like this:

 var actual = getResource(primary);
    var desired = desired(primary, context);
    if (actual.isEmpty()) {
      create(desired, primary, context);
    } else {
      if (!match(actual.get(), desired, context)) {
        update(actual.get(), desired, primary, context);
      }
    }

What is the most common case, when an controller owns the resources, and reconciles it. Whit optinal desired, we cover that and some additional more exotic case, what could desire a completely different abstraction, since we don't just kubernetes but also other external resources that will implement that. In this case this is the read only. So I don't understand this part: Why would we combine exactly these two use cases? So the only editable is not there, how that should there added?

I'm not sure how returning Optional breaks the abstraction. It only makes things more precise by letting the dependent communicate to the SDK that it doesn't have a desired state at this point in time. This works equally as well for non-Kubernetes resources.

The editable-only case would be supported as follows: desired would return empty until the dependent has been created externally. With the following implementation, create would not be called until desired until the resource would be ready to be updated:

     var maybeDesired = desired(primary, context);
     maybeDesired.ifPresent(desired -> {
       var maybeActual = getResource(primary);
       if (maybeActual.isEmpty()) {
         create(desired, primary, context);
       } else {
         final var actual = maybeActual.get();
         if (!match(actual, desired, context)) {
           update(actual, desired, primary, context);
         }
       }
     });

This is, indeed, a little awkward because this would mean that desired would need to check whether the resource exists or not.

However, in the current implementation, the abstraction is ill-defined, because why should editable-only be configured via annotation while read-only is implemented by a subclass / marker interface. So there's an asymmetry here that shouldn't be there. If one feature is configured by annotation, then the other should be as well. If the feature is implemented by a subclass then both should be as well, and in this case, this means adding more classes to support the combinations (i.e. you would need an EditableOnly version of the classes and a version for CreatableOnly for the case where you only want to create the dependent and never need to update it). I still think that a trait-based implementation would be better. Going to see how this goes.

So what I try to say, is the good abstractions of the problems/cases should have the highest priority. And here the generic and the read-only cases deserves it's own abstraction under dependent resource. At least for me this is much more important than having done the current scope (that will probably be extended) with some adjustments to abstractions that has well defined scopes.

Except that the abstraction is not good or at least doesn't work for all cases. Why should read-only be more important than editable-only or creatable-only? The current implementation doesn't address this problem. I do agree, though, that returning Optional for desired probably isn't the best abstraction as it's not completely explicit. That said, you could create a class hierarchy that provides the proper behavior based on this, if you really wanted to (which I don't want to) 😄:

  • read-only: just return empty for desired all the time
  • create-only: return true for match all the time
  • editable-only: return empty for desired until the resource exists (implies that desired more or less needs to call getResource)

This doesn't overload the concepts, it's very natural for an implementation to not have a desired state

It's not that natural, basically then it's not a dependent resource, it's an informer. Not sure if we should actually even solve this. I mean we have to just because we want to have it nicely done with annotations, without that actually we don't even need this. We could just reference the informer from the dependent resource what is using it as an input.

This just isn't true. It often happens when you reconcile dependents that there just isn't any valid state to create/update at some point in time. Granted, though, that enters the domain of managing dependencies between dependents.

@scrocquesel
Copy link
Contributor

Trying to keep pace.

Readonly(informer only) resource is something a reconciler needs to know the state. So in this sense, it's a dependent but it can't be reconciled. It's not depending on its state, it is intrisics. And to enforce that, its public contract should not expose reconcile/delete. Having a base class that expose reconcile/delete from the ground up tell that every subclass can be reconciled which is not true. And as such, whatever that used them must reconcile them and delete.

I understand the need for being standalone but can some logic be put in the manager ? This way, only traits can be on the dependent ? Trying to put the managing code at the same place as the behavior itself made things not SRP and force to have a base class with all the complexity.

I agree that clean separation of concern will add more code and classes. There is a tradeoff between less code and clean object modeling.

public interface DependentResource<R, P extends HasMetadata> {
  Optional<R> getResource(P primaryResource);
}

public interface Desiring { R desired(...); }
public interface Create implements Desiring {  void create(...); }
public interface Update implements Desiring {  void update(...); bool match(...); }
public interface Delete{  void delete(...); }

public abstract class KubernetesDependentResource<R, P extends HasMetadata> implements
   DependentResource, EventSourceProvider, Create, Update, Delete {
}

public DependentResourceManager {

@Override
  public UpdateControl<P> reconcile(P resource, Context context) {
    initContextIfNeeded(resource, context);
    dependents.forEach(dependent -> {
      if (dependent instanceof Create/Update) {
        var maybeActual = getResource(primary);
        var desired = desired(primary, context);
        if (maybeActual.isEmpty()) {
          if (dependent instanceof Create) {
            creator.create(desired, primary, context);
          }
        } else if (dependent instanceof Update ) {
          final var actual = maybeActual.get();
          if (!matcher.match(actual, desired, context)) {
            updater.update(actual, desired, primary, context);
          }
        }
      }
      });
    return UpdateControl.noUpdate();
  }

  @Override
  public DeleteControl cleanup(P resource, Context context) {
    initContextIfNeeded(resource, context);
    dependents.stream()
        .filter(dependent -> dependent instanceof Delete)
        .forEach(Delete::delete);
    return Reconciler.super.cleanup(resource, context);
  }

In this case, there is no need for default/noop behavior in the class hierarchy, if needed, another manager can handle different use case of mixins. I can even enrich the logic and reuse the existing class. For exemple, adding else case (#957).

But then, what is standalone is not the dependent itself but it's manager.

@csviri csviri closed this Feb 23, 2022
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.

Make it possible to have dependents that are never created or updated
3 participants