-
Notifications
You must be signed in to change notification settings - Fork 220
Dependent resources standalone mode #914
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
Changes from all commits
5628db5
0d2363c
6dff17b
ba619e5
dfa2388
1ad4a12
4b089c1
b3edc69
7b73814
76a6dc6
04f76f9
2d3289b
07b4834
850a2ff
68a2619
73c76dc
64b7148
a3c7119
c22c295
3251067
a3fb40f
7843bbc
d851321
816757c
5e25afa
da62a12
1dc2d88
7840238
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package io.javaoperatorsdk.operator.api.reconciler.dependent; | ||
|
||
import io.fabric8.kubernetes.api.model.HasMetadata; | ||
import io.javaoperatorsdk.operator.api.reconciler.Context; | ||
|
||
public abstract class AbstractDependentResource<R, P extends HasMetadata> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it stays as a Factory (i.e. not exist = create) it should be renamed. And sharing version could be added (i.e. not exist = update status with missing target condition). The sharing version is another beast because, delete is about undoing what reconcile does not deleting. Again thinks of https://github.com/redhat-developer/service-binding-operator. When the CR is removed, the deployment is updated to suppress any env var that has been injected. |
||
implements DependentResource<R, P> { | ||
|
||
@Override | ||
public void reconcile(P primary, Context context) { | ||
var actual = getResource(primary); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it's other way around, to for now the event sources are caching the resource ( this is an interesting thing we might want to re-evaluate this later, to separate these two ). But basically at the end the dependent resource should manage it itself, either having an event source (or directly calling the target API). So IMHO it should not have the context in getResource() basically forcing this pattern. (See There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest to rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On level above on DependentResource we don't have desired, so it's really about reading the resource and the desired here is just a way to generically implement it. Hmm will think about it, so yes, it's always actual, not sure if needs to be that explicit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @metacosm what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
var desired = desired(primary, context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should avoid creating the desired version if it's not needed because it could be costly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far I can see it is always used on both branches. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this implementation, yes, that wasn't the case before :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would still like to see this addressed if possible… In a subsequent PR, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can take a look together, not sure how you mean it. |
||
if (actual.isEmpty()) { | ||
csviri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
create(desired, primary, context); | ||
} else { | ||
if (!match(actual.get(), desired, context)) { | ||
update(actual.get(), desired, primary, context); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if match doesn't assert that it has been reconciled. The secondary R may exist prior to the primary one. We should be able to put in the context what has been observed to be desired, so that the reconciler can update the status if it is empty. In the case of mysql schema operator, if the database exists when the primary is created on the cluster, the status is never updated. But, it tricky here because the user is not symetric due to password. if we split, schema and user in two different dependent. Thus, User::match cannot be sure actual = desired because the password may not be the desired one. So if the user exist prior to the CR, either it should fail the reconcile or the password should be changed and secret updated/recreated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not sure I'm following this. We read the R, and compare it to a target state, this is how P gets reconciled (at least this is a part of it). In MySQL there is no update now available for the password. But the whole mysql we will revisit shortly. |
||
} | ||
} | ||
|
||
protected abstract R desired(P primary, Context context); | ||
|
||
protected abstract boolean match(R actual, R target, Context context); | ||
|
||
protected abstract R create(R target, P primary, Context context); | ||
|
||
// the actual needed to copy/preserve new labels or annotations | ||
protected abstract R update(R actual, R target, P primary, Context context); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package io.javaoperatorsdk.operator.api.reconciler.dependent; | ||
|
||
import io.javaoperatorsdk.operator.api.reconciler.Context; | ||
|
||
@FunctionalInterface | ||
public interface DesiredSupplier<R, P> { | ||
|
||
R getDesired(P primary, Context context); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package io.javaoperatorsdk.operator.api.reconciler.dependent; | ||
|
||
import java.util.Optional; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import io.fabric8.kubernetes.api.model.HasMetadata; | ||
import io.fabric8.kubernetes.client.KubernetesClient; | ||
import io.javaoperatorsdk.operator.ReconcilerUtils; | ||
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; | ||
import io.javaoperatorsdk.operator.api.reconciler.Context; | ||
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; | ||
import io.javaoperatorsdk.operator.processing.event.ResourceID; | ||
import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; | ||
import io.javaoperatorsdk.operator.processing.event.source.EventSource; | ||
import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; | ||
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; | ||
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; | ||
|
||
public abstract class KubernetesDependentResource<R extends HasMetadata, P extends HasMetadata> | ||
extends AbstractDependentResource<R, P> { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); | ||
|
||
protected KubernetesClient client; | ||
private boolean explicitDelete = false; | ||
private boolean owned = true; | ||
private InformerEventSource<R, P> informerEventSource; | ||
|
||
public KubernetesDependentResource() { | ||
this(null); | ||
} | ||
|
||
public KubernetesDependentResource(KubernetesClient client) { | ||
this.client = client; | ||
} | ||
|
||
protected void beforeCreateOrUpdate(R desired, P primary) { | ||
if (owned) { | ||
desired.addOwnerReference(primary); | ||
} | ||
} | ||
|
||
@Override | ||
protected boolean match(R actual, R target, Context context) { | ||
return ReconcilerUtils.specsEqual(actual, target); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
@Override | ||
protected R create(R target, P primary, Context context) { | ||
log.debug("Creating target resource with type: " + | ||
"{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); | ||
beforeCreateOrUpdate(target, primary); | ||
Class<R> targetClass = (Class<R>) target.getClass(); | ||
return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) | ||
.create(target); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
@Override | ||
protected R update(R actual, R target, P primary, Context context) { | ||
log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), | ||
ResourceID.fromResource(target)); | ||
beforeCreateOrUpdate(target, primary); | ||
Class<R> targetClass = (Class<R>) target.getClass(); | ||
return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) | ||
.replace(target); | ||
} | ||
|
||
@SuppressWarnings({"unchecked", "rawtypes"}) | ||
@Override | ||
public Optional<EventSource> eventSource(EventSourceContext<P> context) { | ||
if (informerEventSource != null) { | ||
return Optional.of(informerEventSource); | ||
} | ||
var informerConfig = initInformerConfiguration(context); | ||
informerEventSource = new InformerEventSource(informerConfig, context); | ||
return Optional.of(informerEventSource); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private InformerConfiguration<R, P> initInformerConfiguration(EventSourceContext<P> context) { | ||
PrimaryResourcesRetriever<R> associatedPrimaries = | ||
(this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever<R>) this | ||
: getDefaultPrimaryResourcesRetriever(); | ||
|
||
AssociatedSecondaryResourceIdentifier<P> associatedSecondary = | ||
(this instanceof AssociatedSecondaryResourceIdentifier) | ||
? (AssociatedSecondaryResourceIdentifier<P>) this | ||
: getDefaultAssociatedSecondaryResourceIdentifier(); | ||
|
||
return InformerConfiguration.from(context, resourceType()) | ||
.withPrimaryResourcesRetriever(associatedPrimaries) | ||
.withAssociatedSecondaryResourceIdentifier(associatedSecondary) | ||
.build(); | ||
} | ||
|
||
protected AssociatedSecondaryResourceIdentifier<P> getDefaultAssociatedSecondaryResourceIdentifier() { | ||
return ResourceID::fromResource; | ||
} | ||
|
||
protected PrimaryResourcesRetriever<R> getDefaultPrimaryResourcesRetriever() { | ||
return Mappers.fromOwnerReference(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if
As there is two distinct behaviors, it can be exposed through two different classes ? It will be less prone to misconfiguration. But I see there is already a discussion around hierarchy vs configuration. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this two behaviors should be two classes, the current aim but we will refine this (just FYI, this is the first step), see issues (also pls comment in case ) for MVP: #923 Owner reference works just in same namespace, if some dependents are managed outside of namespace those needs to be explicitly created. Maybe it would make sense to subclass it yes. Definetely will make the config more explicit, this is quite vague this way, I agree. |
||
} | ||
|
||
public KubernetesDependentResource<R, P> setInformerEventSource( | ||
InformerEventSource<R, P> informerEventSource) { | ||
this.informerEventSource = informerEventSource; | ||
return this; | ||
} | ||
|
||
@Override | ||
public void delete(P primary, Context context) { | ||
if (explicitDelete) { | ||
var resource = getResource(primary); | ||
resource.ifPresent(r -> client.resource(r).delete()); | ||
} | ||
} | ||
|
||
@Override | ||
public Optional<R> getResource(P primaryResource) { | ||
return informerEventSource.getAssociated(primaryResource); | ||
} | ||
|
||
public KubernetesDependentResource<R, P> setClient(KubernetesClient client) { | ||
this.client = client; | ||
return this; | ||
} | ||
|
||
|
||
public KubernetesDependentResource<R, P> setExplicitDelete(boolean explicitDelete) { | ||
this.explicitDelete = explicitDelete; | ||
return this; | ||
} | ||
|
||
public boolean isExplicitDelete() { | ||
return explicitDelete; | ||
} | ||
|
||
public boolean isOwned() { | ||
return owned; | ||
} | ||
|
||
public KubernetesDependentResource<R, P> setOwned(boolean owned) { | ||
this.owned = owned; | ||
return this; | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package io.javaoperatorsdk.operator.api.reconciler.dependent; | ||
|
||
import io.fabric8.kubernetes.api.model.HasMetadata; | ||
import io.fabric8.kubernetes.client.KubernetesClient; | ||
import io.javaoperatorsdk.operator.api.reconciler.Context; | ||
import io.javaoperatorsdk.operator.processing.event.ResourceID; | ||
import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; | ||
import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; | ||
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; | ||
|
||
// todo shorter name | ||
public class StandaloneKubernetesDependentResource<R extends HasMetadata, P extends HasMetadata> | ||
extends KubernetesDependentResource<R, P> { | ||
|
||
private final DesiredSupplier<R, P> desiredSupplier; | ||
private final Class<R> resourceType; | ||
private AssociatedSecondaryResourceIdentifier<P> associatedSecondaryResourceIdentifier = | ||
ResourceID::fromResource; | ||
private PrimaryResourcesRetriever<R> primaryResourcesRetriever = Mappers.fromOwnerReference(); | ||
|
||
public StandaloneKubernetesDependentResource( | ||
Class<R> resourceType, DesiredSupplier<R, P> desiredSupplier) { | ||
this(null, resourceType, desiredSupplier); | ||
} | ||
|
||
public StandaloneKubernetesDependentResource( | ||
KubernetesClient client, Class<R> resourceType, DesiredSupplier<R, P> desiredSupplier) { | ||
super(client); | ||
this.desiredSupplier = desiredSupplier; | ||
this.resourceType = resourceType; | ||
} | ||
|
||
@Override | ||
protected R desired(P primary, Context context) { | ||
return desiredSupplier.getDesired(primary, context); | ||
} | ||
|
||
public Class<R> resourceType() { | ||
return resourceType; | ||
} | ||
|
||
public StandaloneKubernetesDependentResource<R, P> setAssociatedSecondaryResourceIdentifier( | ||
AssociatedSecondaryResourceIdentifier<P> associatedSecondaryResourceIdentifier) { | ||
this.associatedSecondaryResourceIdentifier = associatedSecondaryResourceIdentifier; | ||
return this; | ||
} | ||
|
||
public StandaloneKubernetesDependentResource<R, P> setPrimaryResourcesRetriever( | ||
PrimaryResourcesRetriever<R> primaryResourcesRetriever) { | ||
this.primaryResourcesRetriever = primaryResourcesRetriever; | ||
return this; | ||
} | ||
|
||
@Override | ||
protected AssociatedSecondaryResourceIdentifier<P> getDefaultAssociatedSecondaryResourceIdentifier() { | ||
return this.associatedSecondaryResourceIdentifier; | ||
} | ||
|
||
@Override | ||
protected PrimaryResourcesRetriever<R> getDefaultPrimaryResourcesRetriever() { | ||
return this.primaryResourcesRetriever; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.