chore(system): implements soft deletion consistenly accross entities

This commit is contained in:
Loïc Mathieu
2025-11-18 12:28:42 +01:00
parent 5d5165b7b9
commit 11e199da33
18 changed files with 80 additions and 58 deletions

View File

@@ -1,5 +0,0 @@
package io.kestra.core.models;
public interface DeletedInterface {
boolean isDeleted();
}

View File

@@ -0,0 +1,18 @@
package io.kestra.core.models;
/**
* This interface marks entities that implement a soft deletion mechanism.
* Soft deletion is based on a <code>deleted</code> field that is set to <code>true</code> when the entity is deleted.
* Physical deletion either never occurs or occurs in a dedicated purge mechanism.
*/
public interface SoftDeletion<T> {
/**
* Whether en entity is deleted or not.
*/
boolean isDeleted();
/**
* Delete the current entity: set its <code>deleted</code> field to <code>true</code>.
*/
T toDeleted();
}

View File

@@ -1,7 +1,7 @@
package io.kestra.core.models.dashboards;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.dashboards.charts.Chart;
import io.kestra.core.utils.IdUtils;
@@ -26,7 +26,7 @@ import java.util.Objects;
@NoArgsConstructor
@Introspected
@ToString
public class Dashboard implements HasUID, DeletedInterface {
public class Dashboard implements HasUID, SoftDeletion<Dashboard> {
@Hidden
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
private String tenantId;
@@ -71,6 +71,7 @@ public class Dashboard implements HasUID, DeletedInterface {
);
}
@Override
public Dashboard toDeleted() {
return this.toBuilder()
.deleted(true)

View File

@@ -11,7 +11,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import io.kestra.core.debug.Breakpoint;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.Label;
import io.kestra.core.models.TenantInterface;
import io.kestra.core.models.flows.Flow;
@@ -53,7 +53,7 @@ import java.util.zip.CRC32;
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class Execution implements DeletedInterface, TenantInterface {
public class Execution implements SoftDeletion<Execution>, TenantInterface {
@With
@Hidden
@@ -1111,7 +1111,7 @@ public class Execution implements DeletedInterface, TenantInterface {
.toList();
}
@Override
public Execution toDeleted() {
return this.toBuilder()
.deleted(true)

View File

@@ -342,6 +342,7 @@ public class Flow extends AbstractFlow implements HasUID {
}
}
@Override
public Flow toDeleted() {
return this.toBuilder()
.revision(this.revision + 1)

View File

@@ -58,4 +58,9 @@ public class FlowForExecution extends AbstractFlow {
public String getSource() {
return null;
}
@Override
public FlowForExecution toDeleted() {
throw new UnsupportedOperationException("Can't delete a FlowForExecution");
}
}

View File

@@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.HasSource;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.Label;
@@ -27,7 +27,7 @@ import java.util.stream.Collectors;
* The base interface for FLow.
*/
@JsonDeserialize(as = GenericFlow.class)
public interface FlowInterface extends FlowId, DeletedInterface, TenantInterface, HasUID, HasSource {
public interface FlowInterface extends FlowId, SoftDeletion<FlowInterface>, TenantInterface, HasUID, HasSource {
Pattern YAML_REVISION_MATCHER = Pattern.compile("(?m)^revision: \\d+\n?");

View File

@@ -96,4 +96,9 @@ public class GenericFlow extends AbstractFlow implements HasUID {
public List<GenericTrigger> getTriggers() {
return Optional.ofNullable(triggers).orElse(List.of());
}
@Override
public FlowInterface toDeleted() {
throw new UnsupportedOperationException("Can't delete a GenericFlow");
}
}

View File

@@ -1,6 +1,6 @@
package io.kestra.core.models.kv;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.TenantInterface;
import io.kestra.core.storages.kv.KVEntry;
@@ -22,7 +22,7 @@ import java.util.Optional;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@ToString
@EqualsAndHashCode
public class PersistedKvMetadata implements DeletedInterface, TenantInterface, HasUID {
public class PersistedKvMetadata implements SoftDeletion<PersistedKvMetadata>, TenantInterface, HasUID {
@With
@Hidden
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
@@ -83,6 +83,7 @@ public class PersistedKvMetadata implements DeletedInterface, TenantInterface, H
return this.toBuilder().updated(Instant.now()).last(true).build();
}
@Override
public PersistedKvMetadata toDeleted() {
return this.toBuilder().updated(Instant.now()).deleted(true).build();
}

View File

@@ -17,8 +17,4 @@ public class Namespace implements NamespaceInterface {
@NotNull
@Pattern(regexp="^[a-z0-9][a-z0-9._-]*")
protected String id;
@NotNull
@Builder.Default
boolean deleted = false;
}

View File

@@ -1,9 +1,8 @@
package io.kestra.core.models.namespaces;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.HasUID;
public interface NamespaceInterface extends DeletedInterface, HasUID {
public interface NamespaceInterface extends HasUID {
String getId();

View File

@@ -2,8 +2,8 @@ package io.kestra.core.models.namespaces.files;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.SoftDeletable;
import io.kestra.core.models.TenantInterface;
import io.kestra.core.storages.FileAttributes;
import io.kestra.core.storages.NamespaceFile;
@@ -24,7 +24,7 @@ import java.time.Instant;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@ToString
@EqualsAndHashCode
public class NamespaceFileMetadata implements DeletedInterface, TenantInterface, HasUID {
public class NamespaceFileMetadata implements SoftDeletable<NamespaceFileMetadata>, TenantInterface, HasUID {
@With
@Hidden
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
@@ -116,6 +116,7 @@ public class NamespaceFileMetadata implements DeletedInterface, TenantInterface,
return this.toBuilder().updated(saveDate).last(true).build();
}
@Override
public NamespaceFileMetadata toDeleted() {
return this.toBuilder().deleted(true).updated(Instant.now()).build();
}

View File

@@ -7,7 +7,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.TenantInterface;
import io.kestra.core.models.tasks.Task;
@@ -35,7 +35,7 @@ import jakarta.validation.constraints.Pattern;
@Introspected
@ToString
@EqualsAndHashCode
public class Template implements DeletedInterface, TenantInterface, HasUID {
public class Template implements SoftDeletion<Template>, TenantInterface, HasUID {
private static final ObjectMapper YAML_MAPPER = JacksonMapper.ofYaml().copy()
.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
@Override
@@ -141,6 +141,7 @@ public class Template implements DeletedInterface, TenantInterface, HasUID {
}
}
@Override
public Template toDeleted() {
return new Template(
this.tenantId,

View File

@@ -36,7 +36,7 @@ public interface KvMetadataRepositoryInterface extends SaveRepositoryInterface<P
);
default PersistedKvMetadata delete(PersistedKvMetadata persistedKvMetadata) throws IOException {
return this.save(persistedKvMetadata.toBuilder().deleted(true).build());
return this.save(persistedKvMetadata.toDeleted());
}
/**

View File

@@ -1,7 +1,7 @@
package io.kestra.core.test;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.HasSource;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.TenantInterface;
@@ -25,7 +25,7 @@ import java.util.List;
@ToString
@EqualsAndHashCode
@TestSuiteValidation
public class TestSuite implements HasUID, TenantInterface, DeletedInterface, HasSource {
public class TestSuite implements HasUID, TenantInterface, SoftDeletion<TestSuite>, HasSource {
@NotNull
@NotBlank
@@ -85,10 +85,6 @@ public class TestSuite implements HasUID, TenantInterface, DeletedInterface, Has
);
}
public TestSuite delete() {
return this.toBuilder().deleted(true).build();
}
public TestSuite disable() {
var disabled = true;
return this.toBuilder()
@@ -120,4 +116,9 @@ public class TestSuite implements HasUID, TenantInterface, DeletedInterface, Has
return yamlSource + String.format("\ndisabled: %s", disabled);
}
@Override
public TestSuite toDeleted() {
return toBuilder().deleted(true).build();
}
}

View File

@@ -1,6 +1,6 @@
package io.kestra.core.test;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.SoftDeletion;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.TenantInterface;
import io.kestra.core.test.flow.UnitTestResult;
@@ -24,7 +24,7 @@ public record TestSuiteRunEntity(
String flowId,
TestState state,
List<UnitTestResult> results
) implements DeletedInterface, TenantInterface, HasUID {
) implements SoftDeletion<TestSuiteRunEntity>, TenantInterface, HasUID {
public static TestSuiteRunEntity create(String tenantId, TestSuiteUid testSuiteUid, TestSuiteRunResult testSuiteRunResult) {
return new TestSuiteRunEntity(
@@ -43,23 +43,6 @@ public record TestSuiteRunEntity(
);
}
public TestSuiteRunEntity delete() {
return new TestSuiteRunEntity(
this.uid,
this.id,
this.tenantId,
true,
this.startDate,
this.endDate,
this.testSuiteId,
this.testSuiteUid,
this.namespace,
this.flowId,
this.state,
this.results
);
}
/**
* only used for backup
* @param newTenantId the tenant to migrate to
@@ -86,6 +69,24 @@ public record TestSuiteRunEntity(
return this.deleted;
}
@Override
public TestSuiteRunEntity toDeleted() {
return new TestSuiteRunEntity(
this.uid,
this.id,
this.tenantId,
true,
this.startDate,
this.endDate,
this.testSuiteId,
this.testSuiteUid,
this.namespace,
this.flowId,
this.state,
this.results
);
}
@Override
public String getTenantId() {
return this.tenantId;

View File

@@ -57,7 +57,7 @@ class DashboardControllerTest {
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
@@ -140,7 +140,7 @@ class DashboardControllerTest {
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
@@ -195,7 +195,7 @@ class DashboardControllerTest {
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
@@ -246,7 +246,7 @@ class DashboardControllerTest {
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
@@ -330,7 +330,7 @@ class DashboardControllerTest {
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
@@ -378,7 +378,6 @@ class DashboardControllerTest {
.namespace(fakeNamespace)
.level(Level.INFO)
.attemptNumber(1)
.deleted(false)
.executionId(fakeExecutionId)
.tenantId(MAIN_TENANT)
.executionKind(ExecutionKind.NORMAL)
@@ -447,7 +446,6 @@ class DashboardControllerTest {
.namespace(fakeNamespace)
.level(Level.INFO)
.attemptNumber(1)
.deleted(false)
.executionId(fakeExecutionId)
.tenantId(MAIN_TENANT)
.executionKind(ExecutionKind.NORMAL)

View File

@@ -57,7 +57,6 @@ public class NamespaceControllerTest {
);
assertThat(namespace.getId()).isEqualTo("my.ns");
assertThat(namespace.isDeleted()).isFalse();
}
@SuppressWarnings("unchecked")