Skip to content

feat: status updates #147

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

Merged
merged 8 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ In other words, it also allows you to write **workflows** over resources in a **

## Contact Us

Either in the discussion section here on GitHub or at [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5).
Either in the discussion section here on GitHub or at [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5). While
in "object" form only placeholder substitutions are possible, in string template you can use all the
features of qute.

## Quick Introduction

Expand Down Expand Up @@ -61,6 +63,8 @@ spec:
parent:
apiVersion: glueoperator.sample/v1 # watches all the custom resource of type WebPage
kind: WebPage
statusTemplate: | # update the status of the custom resource at the end of reconciliation
observedGeneration: {parent.metadata.generation}
childResources:
- name: htmlconfigmap
resource:
Expand Down
10 changes: 10 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ of [Java Operator SDK](https://github.com/operator-framework/java-operator-sdk)
Although it is limited only to Kubernetes resources it makes it very easy to use in language-independent
(DependentResources in JOSDK are also covering external resources) way.

## Generic Notes

- All templates (both object and string based) uses [Qute templating engine](https://quarkus.io/guides/qute-reference).

## [Glue resource](https://github.com/java-operator-sdk/kubernetes-glue-operator/releases/latest/download/glues.glue-v1.yml)

`Glue` is the heart of the operator. Note that `GlueOperator` controller just creates a new `Glue` with a related resource,
Expand Down Expand Up @@ -57,6 +61,9 @@ The following attributes can be defined for a related resource:
- **`apiVersion`** - Kubernetes resource API Version of the resource
- **`kind`** - Kubernetes kind property of the resource
- **`resourceNames`** - list of string of the resource names within the same namespace as `Glue`.
- **`statusPatch`** - template object used to update status of the related resource at the end of the reconciliation. See [sample](https://github.com/java-operator-sdk/kubernetes-glue-operator/blob/main/src/test/resources/glue/PatchRelatedStatus.yaml#L20-L21).
All the available resources (child, related) are provided.
- **`statusPatchTemplate`** - same as `statusPatch` just as a string template. See [sample](https://github.com/java-operator-sdk/kubernetes-glue-operator/blob/main/src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml#L20-L21).

### Referencing other resources

Expand Down Expand Up @@ -91,6 +98,9 @@ The specs of `GlueOperator` are almost identical to `Glue`, it just adds some ad
- **`apiVersion`** and **`kind`** - of the target custom resources.
- **`labelSelector`** - optional label selector for the target resources.
- **`clusterScoped`** - optional boolean value, if the parent resource is cluster scoped. Default is `false`.
- **`status`** - template object to update status of the related resource at the end of the reconciliation.
All the available resources (parent, child, related) are available.
- **`statusTemplate`** - same as `status` just as a string template.
- **`glueMetadata`** - optionally, you can customize the `Glue` resource created for each parent resource.
This is especially important when the parent is a cluster scoped resource - in that case it is mandatory to set.
Using this you can specify the **`name`** and **`namespace`** of the created `Glue`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.Objects;

import io.fabric8.crd.generator.annotation.PreserveUnknownFields;
import io.fabric8.generator.annotation.Required;

public class RelatedResourceSpec {
Expand All @@ -19,6 +20,9 @@ public class RelatedResourceSpec {
private boolean clusterScoped = Boolean.FALSE;
private List<String> resourceNames;

@PreserveUnknownFields
private Object statusPatch;
private String statusPatchTemplate;

public String getApiVersion() {
return apiVersion;
Expand Down Expand Up @@ -73,14 +77,16 @@ public boolean equals(Object o) {
return false;
RelatedResourceSpec that = (RelatedResourceSpec) o;
return clusterScoped == that.clusterScoped && Objects.equals(name, that.name)
&& Objects.equals(apiVersion, that.apiVersion) && Objects.equals(kind, that.kind)
&& Objects.equals(namespace, that.namespace)
&& Objects.equals(resourceNames, that.resourceNames);
&& Objects.equals(namespace, that.namespace) && Objects.equals(apiVersion, that.apiVersion)
&& Objects.equals(kind, that.kind) && Objects.equals(resourceNames, that.resourceNames)
&& Objects.equals(statusPatch, that.statusPatch)
&& Objects.equals(statusPatchTemplate, that.statusPatchTemplate);
}

@Override
public int hashCode() {
return Objects.hash(name, apiVersion, kind, clusterScoped, namespace, resourceNames);
return Objects.hash(name, namespace, apiVersion, kind, clusterScoped, resourceNames,
statusPatch, statusPatchTemplate);
}

public boolean isClusterScoped() {
Expand All @@ -90,4 +96,20 @@ public boolean isClusterScoped() {
public void setClusterScoped(boolean clusterScoped) {
this.clusterScoped = clusterScoped;
}

public Object getStatusPatch() {
return statusPatch;
}

public void setStatusPatch(Object statusPatch) {
this.statusPatch = statusPatch;
}

public String getStatusPatchTemplate() {
return statusPatchTemplate;
}

public void setStatusPatchTemplate(String statusPatchTemplate) {
this.statusPatchTemplate = statusPatchTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import java.util.Objects;

import io.fabric8.crd.generator.annotation.PreserveUnknownFields;

public class Parent {

private String apiVersion;
private String kind;
private boolean clusterScoped = false;
private String labelSelector;

@PreserveUnknownFields
private Object status;
private String statusTemplate;

public Parent() {}

Expand Down Expand Up @@ -51,6 +56,22 @@ public void setClusterScoped(boolean clusterScoped) {
this.clusterScoped = clusterScoped;
}

public Object getStatus() {
return status;
}

public void setStatus(Object status) {
this.status = status;
}

public String getStatusTemplate() {
return statusTemplate;
}

public void setStatusTemplate(String statusTemplate) {
this.statusTemplate = statusTemplate;
}

@Override
public boolean equals(Object o) {
if (this == o)
Expand All @@ -59,11 +80,13 @@ public boolean equals(Object o) {
return false;
Parent parent = (Parent) o;
return clusterScoped == parent.clusterScoped && Objects.equals(apiVersion, parent.apiVersion)
&& Objects.equals(kind, parent.kind) && Objects.equals(labelSelector, parent.labelSelector);
&& Objects.equals(kind, parent.kind) && Objects.equals(labelSelector, parent.labelSelector)
&& Objects.equals(status, parent.status)
&& Objects.equals(statusTemplate, parent.statusTemplate);
}

@Override
public int hashCode() {
return Objects.hash(apiVersion, kind, clusterScoped, labelSelector);
return Objects.hash(apiVersion, kind, clusterScoped, labelSelector, status, statusTemplate);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
import io.fabric8.kubernetes.client.dsl.base.PatchType;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.glue.Utils;
import io.javaoperatorsdk.operator.glue.conditions.JavaScripCondition;
Expand Down Expand Up @@ -89,6 +90,7 @@ public UpdateControl<Glue> reconcile(Glue primary,
cleanupRemovedResourcesFromWorkflow(context, primary);
informerRegister.deRegisterInformerOnResourceFlowChange(context, primary);
result.throwAggregateExceptionIfErrorsPresent();
patchRelatedResourcesStatus(context, primary);
return UpdateControl.noUpdate();
}

Expand Down Expand Up @@ -222,6 +224,35 @@ private GenericDependentResource createDependentResource(DependentResourceSpec s
}
}

// todo add workflow result?
private void patchRelatedResourcesStatus(Context<Glue> context,
Glue primary) {

var targetRelatedResources = primary.getSpec().getRelatedResources().stream()
.filter(r -> r.getStatusPatch() != null || r.getStatusPatchTemplate() != null)
.toList();

if (targetRelatedResources.isEmpty()) {
return;
}
var actualData = genericTemplateHandler.createDataWithResources(primary, context);

targetRelatedResources.forEach(r -> {
var relatedResources = Utils.getRelatedResources(primary, r, context);

var template = r.getStatusPatchTemplate() != null ? r.getStatusPatchTemplate()
: Serialization.asYaml(r.getStatusPatch());
var resultTemplate =
genericTemplateHandler.processTemplate(actualData, template);
var statusObjectMap = GenericTemplateHandler.parseTemplateToMapObject(resultTemplate);
relatedResources.forEach((n, kr) -> {
kr.setAdditionalProperty("status", statusObjectMap);
context.getClient().resource(kr).patchStatus();
});
});

}

@SuppressWarnings({"rawtypes"})
private Condition toCondition(ConditionSpec condition) {
if (condition instanceof ReadyConditionSpec readyConditionSpec) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.javaoperatorsdk.operator.glue.customresource.glue.RelatedResourceSpec;
import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperator;
import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperatorSpec;
import io.javaoperatorsdk.operator.glue.customresource.operator.Parent;
import io.javaoperatorsdk.operator.glue.customresource.operator.ResourceFlowOperatorStatus;
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
import io.javaoperatorsdk.operator.glue.reconciler.glue.GlueReconciler;
Expand Down Expand Up @@ -103,7 +104,6 @@ private Glue createGlue(GenericKubernetesResource targetParentResource,

ObjectMeta glueMetadata = glueMetadata(glueOperator, targetParentResource);


glue.setMetadata(glueMetadata);
glue.setSpec(toWorkflowSpec(glueOperator.getSpec()));

Expand All @@ -112,17 +112,27 @@ private Glue createGlue(GenericKubernetesResource targetParentResource,
}

var parent = glueOperator.getSpec().getParent();
RelatedResourceSpec parentRelatedSpec =
parentRelatedResourceSpec(targetParentResource, glueOperator, parent);

glue.getSpec().getRelatedResources().add(parentRelatedSpec);
glue.addOwnerReference(targetParentResource);
return glue;
}

private static RelatedResourceSpec parentRelatedResourceSpec(
GenericKubernetesResource targetParentResource, GlueOperator glueOperator, Parent parent) {
RelatedResourceSpec parentRelatedSpec = new RelatedResourceSpec();
parentRelatedSpec.setName(PARENT_RELATED_RESOURCE_NAME);
parentRelatedSpec.setApiVersion(parent.getApiVersion());
parentRelatedSpec.setKind(parent.getKind());
parentRelatedSpec.setResourceNames(List.of(targetParentResource.getMetadata().getName()));
parentRelatedSpec.setNamespace(targetParentResource.getMetadata().getNamespace());
parentRelatedSpec.setClusterScoped(glueOperator.getSpec().getParent().isClusterScoped());

glue.getSpec().getRelatedResources().add(parentRelatedSpec);
glue.addOwnerReference(targetParentResource);
return glue;
parentRelatedSpec
.setStatusPatchTemplate(glueOperator.getSpec().getParent().getStatusTemplate());
parentRelatedSpec.setStatusPatch(glueOperator.getSpec().getParent().getStatus());
return parentRelatedSpec;
}

private ObjectMeta glueMetadata(GlueOperator glueOperator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Map;

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.glue.Utils;
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
Expand All @@ -29,20 +30,26 @@ public String processTemplate(Map<String, Map<?, ?>> data, String template) {

public String processInputAndTemplate(Map<String, GenericKubernetesResource> data,
String template) {
Map<String, Map<?, ?>> res = new HashMap<>();
data.forEach((key, value) -> res.put(key,
value == null ? null : objectMapper.convertValue(value, Map.class)));
Map<String, Map<?, ?>> res =
genericKubernetesResourceDataToGenericData(data);
return processTemplate(res, template);
}

public String processTemplate(String template, Glue primary,
Context<Glue> context) {

var data = createDataWithResources(primary, context);
return processTemplate(data, template);
}

private static Map<String, Map<?, ?>> createDataWithResources(Glue primary,
private static Map<String, Map<?, ?>> genericKubernetesResourceDataToGenericData(
Map<String, GenericKubernetesResource> data) {
Map<String, Map<?, ?>> res = new HashMap<>();
data.forEach((key, value) -> res.put(key,
value == null ? null : objectMapper.convertValue(value, Map.class)));
return res;
}

public static Map<String, Map<?, ?>> createDataWithResources(Glue primary,
Context<Glue> context) {
Map<String, Map<?, ?>> res = new HashMap<>();
var actualResourcesByName = Utils.getActualResourcesByNameInWorkflow(context, primary);
Expand All @@ -55,4 +62,10 @@ public String processTemplate(String template, Glue primary,

return res;
}

@SuppressWarnings("unchecked")
public static Map<String, ?> parseTemplateToMapObject(String template) {
return Serialization.unmarshal(template, Map.class);
}

}
30 changes: 30 additions & 0 deletions src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
import io.javaoperatorsdk.operator.glue.customresource.TestCustomResource;
import io.javaoperatorsdk.operator.glue.customresource.glue.DependentResourceSpec;
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
Expand Down Expand Up @@ -324,6 +327,33 @@ void clusterScopedRelatedResource() {
});
}

@ParameterizedTest
@ValueSource(strings = {"PatchRelatedStatus.yaml", "PatchRelatedStatusWithTemplate.yaml"})
void pathRelatedResourceStatus(String glueFileName) {
TestUtils.applyTestCrd(client, TestCustomResource.class);

var customResource = create(TestData.testCustomResource());
var glue = createGlue("/glue/" + glueFileName);

await().untilAsserted(() -> {
var cm = get(ConfigMap.class, "configmap1");
assertThat(cm).isNotNull();
var cr = get(TestCustomResource.class, "testcr1");
assertThat(cr.getStatus()).isNotNull();
assertThat(cr.getStatus().getValue()).isEqualTo(cm.getMetadata().getResourceVersion());
});
delete(glue);
await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> {
var cm = get(ConfigMap.class, "configmap1");
assertThat(cm).isNull();
});
delete(customResource);
await().untilAsserted(() -> {
var cr = get(TestCustomResource.class, "testcr1");
assertThat(cr).isNull();
});
}

private List<Glue> testWorkflowList(int num) {
List<Glue> res = new ArrayList<>();
IntStream.range(0, num).forEach(index -> {
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;

import jakarta.inject.Inject;

import static io.javaoperatorsdk.operator.glue.TestUtils.loadGlue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

Expand Down Expand Up @@ -51,6 +53,10 @@ protected Namespace testNamespace(String name) {
.build()).build();
}

protected Glue createGlue(String path) {
return create(loadGlue(path));
}

protected <T extends HasMetadata> T create(T resource) {
return client.resource(resource).inNamespace(testNamespace).create();
}
Expand Down
Loading
Loading