From 78d98b2c7a643d3c845ac4e832c8f7de440b5a34 Mon Sep 17 00:00:00 2001 From: Vaishnav88sk Date: Mon, 2 Nov 2026 15:08:02 +0530 Subject: [PATCH] feat(operator): add keepOnDelete support for artifact resources Signed-off-by: Vaishnav88sk --- .../api/prompts/v1alpha1/PromptsSpec.java | 13 ++++++++++++- .../api/resources/v1alpha1/ResourcesSpec.java | 13 ++++++++++++- .../api/tools/v1alpha1/CustomToolsSpec.java | 13 ++++++++++++- deploy/crd/customtools.reshapr.io-v1.yml | 4 ++++ deploy/crd/prompts.reshapr.io-v1.yml | 4 ++++ deploy/crd/resources.reshapr.io-v1.yml | 4 ++++ documentation/customtools-cr.md | 1 + documentation/prompts-cr.md | 9 +++++---- documentation/resources-cr.md | 1 + .../operator/BaseArtifactReconciler.java | 6 ++++++ .../operator/CustomToolsReconciler.java | 5 +++++ .../kubernetes/operator/PromptsReconciler.java | 5 +++++ .../kubernetes/operator/ResourcesReconciler.java | 5 +++++ .../operator/PromptsReconcilerTest.java | 16 ++++++++++++++++ 14 files changed, 92 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/io/reshapr/kubernetes/api/prompts/v1alpha1/PromptsSpec.java b/api/src/main/java/io/reshapr/kubernetes/api/prompts/v1alpha1/PromptsSpec.java index 696b3f4..7735f5d 100644 --- a/api/src/main/java/io/reshapr/kubernetes/api/prompts/v1alpha1/PromptsSpec.java +++ b/api/src/main/java/io/reshapr/kubernetes/api/prompts/v1alpha1/PromptsSpec.java @@ -30,7 +30,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({ "service", "prompts" }) +@JsonPropertyOrder({ "service", "prompts", "keepOnDelete" }) @Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") public class PromptsSpec { @@ -40,6 +40,9 @@ public class PromptsSpec { @JsonPropertyDescription("The prompts are defined with name as key.") private Map prompts; + @JsonPropertyDescription("Flag to keep artifact in control plane when deleting this resource. Default is false.") + private boolean keepOnDelete = false; + public ServiceRef getService() { return service; } @@ -55,4 +58,12 @@ public Map getPrompts() { public void setPrompts(Map prompts) { this.prompts = prompts; } + + public boolean isKeepOnDelete() { + return keepOnDelete; + } + + public void setKeepOnDelete(boolean keepOnDelete) { + this.keepOnDelete = keepOnDelete; + } } diff --git a/api/src/main/java/io/reshapr/kubernetes/api/resources/v1alpha1/ResourcesSpec.java b/api/src/main/java/io/reshapr/kubernetes/api/resources/v1alpha1/ResourcesSpec.java index 0a100b4..1972663 100644 --- a/api/src/main/java/io/reshapr/kubernetes/api/resources/v1alpha1/ResourcesSpec.java +++ b/api/src/main/java/io/reshapr/kubernetes/api/resources/v1alpha1/ResourcesSpec.java @@ -36,7 +36,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({ "service", "resources", "resourceTemplates" }) +@JsonPropertyOrder({ "service", "resources", "resourceTemplates", "keepOnDelete" }) @Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") public class ResourcesSpec { @@ -49,6 +49,9 @@ public class ResourcesSpec { @JsonPropertyDescription("The resource template definitions, keyed by URI template.") private Map resourceTemplates; + @JsonPropertyDescription("Flag to keep artifact in control plane when deleting this resource. Default is false.") + private boolean keepOnDelete = false; + public ServiceRef getService() { return service; } @@ -72,4 +75,12 @@ public Map getResourceTemplates() { public void setResourceTemplates(Map resourceTemplates) { this.resourceTemplates = resourceTemplates; } + + public boolean isKeepOnDelete() { + return keepOnDelete; + } + + public void setKeepOnDelete(boolean keepOnDelete) { + this.keepOnDelete = keepOnDelete; + } } diff --git a/api/src/main/java/io/reshapr/kubernetes/api/tools/v1alpha1/CustomToolsSpec.java b/api/src/main/java/io/reshapr/kubernetes/api/tools/v1alpha1/CustomToolsSpec.java index 78f962b..8ef3753 100644 --- a/api/src/main/java/io/reshapr/kubernetes/api/tools/v1alpha1/CustomToolsSpec.java +++ b/api/src/main/java/io/reshapr/kubernetes/api/tools/v1alpha1/CustomToolsSpec.java @@ -30,7 +30,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({ "service", "customTools" }) +@JsonPropertyOrder({ "service", "customTools", "keepOnDelete" }) @Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") public class CustomToolsSpec { @@ -40,6 +40,9 @@ public class CustomToolsSpec { @JsonPropertyDescription("The custom tools are defined with name as key.") private Map customTools; + @JsonPropertyDescription("Flag to keep artifact in control plane when deleting this resource. Default is false.") + private boolean keepOnDelete = false; + public ServiceRef getService() { return service; } @@ -55,4 +58,12 @@ public Map getCustomTools() { public void setCustomTools(Map customTools) { this.customTools = customTools; } + + public boolean isKeepOnDelete() { + return keepOnDelete; + } + + public void setKeepOnDelete(boolean keepOnDelete) { + this.keepOnDelete = keepOnDelete; + } } diff --git a/deploy/crd/customtools.reshapr.io-v1.yml b/deploy/crd/customtools.reshapr.io-v1.yml index ec1b3ac..ef1f538 100644 --- a/deploy/crd/customtools.reshapr.io-v1.yml +++ b/deploy/crd/customtools.reshapr.io-v1.yml @@ -79,6 +79,10 @@ spec: type: "object" description: "The custom tools are defined with name as key." type: "object" + keepOnDelete: + description: "Flag to keep artifact in control plane when deleting this\ + \ resource. Default is false" + type: "boolean" service: description: "Holds reference information about this Service custom\ \ tools relate." diff --git a/deploy/crd/prompts.reshapr.io-v1.yml b/deploy/crd/prompts.reshapr.io-v1.yml index e2a4a9d..0a8aa6b 100644 --- a/deploy/crd/prompts.reshapr.io-v1.yml +++ b/deploy/crd/prompts.reshapr.io-v1.yml @@ -19,6 +19,10 @@ spec: properties: spec: properties: + keepOnDelete: + description: "Flag to keep artifact in control plane when deleting this\ + \ resource. Default is false" + type: "boolean" prompts: additionalProperties: properties: diff --git a/deploy/crd/resources.reshapr.io-v1.yml b/deploy/crd/resources.reshapr.io-v1.yml index 435e81c..f29c303 100644 --- a/deploy/crd/resources.reshapr.io-v1.yml +++ b/deploy/crd/resources.reshapr.io-v1.yml @@ -19,6 +19,10 @@ spec: properties: spec: properties: + keepOnDelete: + description: "Flag to keep artifact in control plane when deleting this\ + \ resource. Default is false" + type: "boolean" resourceTemplates: additionalProperties: properties: diff --git a/documentation/customtools-cr.md b/documentation/customtools-cr.md index e0d309e..a2d05c8 100644 --- a/documentation/customtools-cr.md +++ b/documentation/customtools-cr.md @@ -78,6 +78,7 @@ status: |----------------|------------------------------------------------------------------------------------------------------| | `service` | **Mandatory**. Reference to the target Service — see _Service reference_ below. | | `customTools` | **Mandatory**. Map of tool name → [_Custom tool item_](#custom-tool-item-specification). | +| `keepOnDelete` | **Optional**. When `true`, deleting the CR keeps the artifact in the control plane. Defaults to `false`. | ### Service reference (`spec.service`) diff --git a/documentation/prompts-cr.md b/documentation/prompts-cr.md index 33129d0..3fc0398 100644 --- a/documentation/prompts-cr.md +++ b/documentation/prompts-cr.md @@ -47,10 +47,11 @@ mandatory — see the [Instance connection flow](./instance-connection.md). The ## Specification details -| Property | Description | -|-----------|--------------------------------------------------------------------------------------| -| `service` | **Mandatory**. Target Service reference (`name` and `version`). | -| `prompts` | Map of prompt name → prompt item (see below). | +| Property | Description | +|----------------|--------------------------------------------------------------------------------------| +| `service` | **Mandatory**. Target Service reference (`name` and `version`). | +| `prompts` | Map of prompt name → prompt item (see below). | +| `keepOnDelete` | **Optional**. When `true`, deleting the CR keeps the artifact in the control plane. Defaults to `false`. | ### Prompt item diff --git a/documentation/resources-cr.md b/documentation/resources-cr.md index b1d3dab..3e62d96 100644 --- a/documentation/resources-cr.md +++ b/documentation/resources-cr.md @@ -87,6 +87,7 @@ status: | `service` | **Mandatory**. Reference to the target Service — see _Service reference_ below. | | `resources` | **Optional**. Map of URI → [_Resource item_](#resources-item-specification). At least one of `resources` or `resourceTemplates` must be present. | | `resourceTemplates` | **Optional**. Map of URI template → [_Resource template item_](#resources-template-item-specification). At least one of `resources` or `resourceTemplates` must be present. | +| `keepOnDelete` | **Optional**. When `true`, deleting the CR keeps the artifact in the control plane. Defaults to `false`. | ### Service reference (`spec.service`) diff --git a/operator/src/main/java/io/reshapr/kubernetes/operator/BaseArtifactReconciler.java b/operator/src/main/java/io/reshapr/kubernetes/operator/BaseArtifactReconciler.java index 19b902a..e4221ac 100644 --- a/operator/src/main/java/io/reshapr/kubernetes/operator/BaseArtifactReconciler.java +++ b/operator/src/main/java/io/reshapr/kubernetes/operator/BaseArtifactReconciler.java @@ -57,12 +57,18 @@ protected BaseArtifactReconciler(ReshaprApiClientFactory apiClientFactory, Artif protected abstract String getArtifactContent(R resource) throws Exception; protected abstract String getArtifactName(R resource); protected abstract String getArtifactId(R resource); + protected abstract boolean isKeepOnDelete(R resource); protected abstract void updateStatus(R resource, String serviceId, String artifactId, Status status, String message); @Override public DeleteControl cleanup(R resource, Context context) { String name = resource.getMetadata().getName(); + if (isKeepOnDelete(resource)) { + logger.infof("'keepOnDelete' is set for resource '%s' — leaving control plane Artifact in place", name); + return DeleteControl.defaultDelete(); + } + try { ApiClient apiClient = authenticatedClientFor(resource); if (apiClient == null) { diff --git a/operator/src/main/java/io/reshapr/kubernetes/operator/CustomToolsReconciler.java b/operator/src/main/java/io/reshapr/kubernetes/operator/CustomToolsReconciler.java index ea825ba..9cb7555 100644 --- a/operator/src/main/java/io/reshapr/kubernetes/operator/CustomToolsReconciler.java +++ b/operator/src/main/java/io/reshapr/kubernetes/operator/CustomToolsReconciler.java @@ -89,6 +89,11 @@ protected String getArtifactId(CustomTools resource) { return resource.getStatus() != null ? resource.getStatus().getArtifactId() : null; } + @Override + protected boolean isKeepOnDelete(CustomTools resource) { + return resource.getSpec() != null && resource.getSpec().isKeepOnDelete(); + } + @Override protected void updateStatus(CustomTools resource, String serviceId, String artifactId, Status status, String message) { CustomToolsStatus s = resource.getStatus(); diff --git a/operator/src/main/java/io/reshapr/kubernetes/operator/PromptsReconciler.java b/operator/src/main/java/io/reshapr/kubernetes/operator/PromptsReconciler.java index 297e0cc..736ce88 100644 --- a/operator/src/main/java/io/reshapr/kubernetes/operator/PromptsReconciler.java +++ b/operator/src/main/java/io/reshapr/kubernetes/operator/PromptsReconciler.java @@ -92,6 +92,11 @@ protected String getArtifactId(Prompts resource) { return resource.getStatus() != null ? resource.getStatus().getArtifactId() : null; } + @Override + protected boolean isKeepOnDelete(Prompts resource) { + return resource.getSpec() != null && resource.getSpec().isKeepOnDelete(); + } + @Override protected void updateStatus(Prompts resource, String serviceId, String artifactId, Status status, String message) { PromptsStatus s = resource.getStatus(); diff --git a/operator/src/main/java/io/reshapr/kubernetes/operator/ResourcesReconciler.java b/operator/src/main/java/io/reshapr/kubernetes/operator/ResourcesReconciler.java index 6226757..3ca8bc0 100644 --- a/operator/src/main/java/io/reshapr/kubernetes/operator/ResourcesReconciler.java +++ b/operator/src/main/java/io/reshapr/kubernetes/operator/ResourcesReconciler.java @@ -95,6 +95,11 @@ protected String getArtifactId(Resources resources) { return resources.getStatus() != null ? resources.getStatus().getArtifactId() : null; } + @Override + protected boolean isKeepOnDelete(Resources resources) { + return resources.getSpec() != null && resources.getSpec().isKeepOnDelete(); + } + @Override protected void updateStatus(Resources resources, String serviceId, String artifactId, Status status, String message) { ResourcesStatus s = resources.getStatus(); diff --git a/operator/src/test/java/io/reshapr/kubernetes/operator/PromptsReconcilerTest.java b/operator/src/test/java/io/reshapr/kubernetes/operator/PromptsReconcilerTest.java index 593a236..2ac4cc5 100644 --- a/operator/src/test/java/io/reshapr/kubernetes/operator/PromptsReconcilerTest.java +++ b/operator/src/test/java/io/reshapr/kubernetes/operator/PromptsReconcilerTest.java @@ -161,5 +161,21 @@ void testCleanup_handles404Cleanly() throws Exception { verify(mockedApi).deleteArtifact("art-123"); } } + + @Test + void testCleanup_honorsKeepOnDelete() throws Exception { + ReshaprApiClientFactory factory = mock(ReshaprApiClientFactory.class); + PromptsReconciler customReconciler = new PromptsReconciler(factory, null, objectMapper); + Prompts prompts = buildPrompts(); + prompts.getSpec().setKeepOnDelete(true); + customReconciler.updateStatus(prompts, "svc-1", "art-123", Status.READY, "ok"); + + try (MockedConstruction mocked = Mockito.mockConstruction(DefaultApi.class)) { + DeleteControl control = customReconciler.cleanup(prompts, null); + + assertTrue(control.isRemoveFinalizer()); + assertEquals(0, mocked.constructed().size(), "DefaultApi should not be initialized when keepOnDelete is true"); + } + } }