Repository: kestra-io/kestra (java)

Issue:
Mandate at least one condition on flow trigger
### Feature description

Currently, we can create a Flow with a Flow Trigger without any condition.

Such flow will be triggered on each execution of each flow, this is a great way to double the execution number on each flow (in fact increase it exponentially for each flow that have such trigger).

I think it will be best to mandate at least one condition, user that really need this functionality will be able to add a condition that always evaluate to true.

Pull request:
fix(core): require condition in Flow trigger
close #971

Patch:
diff --git a/core/src/main/java/io/kestra/core/services/FlowService.java b/core/src/main/java/io/kestra/core/services/FlowService.java
index be1107cac..c6f562105 100644
--- a/core/src/main/java/io/kestra/core/services/FlowService.java
+++ b/core/src/main/java/io/kestra/core/services/FlowService.java
@@ -128,21 +128,17 @@ public class FlowService {
         return deprecationTraversal("", flow).toList();
     }
 
+    /**
+     * @deprecated as we have no more usaage inside
+     * maybe be kept if we found something to verify
+     */
+    @Deprecated
     public List<String> warnings(Flow flow) {
         if (flow == null) {
             return Collections.emptyList();
         }
 
         List<String> warnings = new ArrayList<>();
-        List<io.kestra.plugin.core.trigger.Flow> flowTriggers = ListUtils.emptyOnNull(flow.getTriggers()).stream()
-            .filter(io.kestra.plugin.core.trigger.Flow.class::isInstance)
-            .map(io.kestra.plugin.core.trigger.Flow.class::cast)
-            .toList();
-        flowTriggers.forEach(flowTrigger -> {
-            if (ListUtils.emptyOnNull(flowTrigger.getConditions()).isEmpty() && flowTrigger.getPreconditions() == null) {
-                warnings.add("This flow will be triggered for EVERY execution of EVERY flow on your instance. We recommend adding the preconditions property to the Flow trigger '" + flowTrigger.getId() + "'.");
-            }
-        });
 
         return warnings;
     }
diff --git a/core/src/main/java/io/kestra/core/validations/ConditionValidation.java b/core/src/main/java/io/kestra/core/validations/ConditionValidation.java
new file mode 100644
index 000000000..637e13d50
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/validations/ConditionValidation.java
@@ -0,0 +1,16 @@
+package io.kestra.core.validations;
+
+import io.kestra.core.validations.validator.ConditionValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = ConditionValidator.class)
+public @interface ConditionValidation {
+    String message() default "one condition must be set";
+    Class<?>[] groups() default {};
+    Class<? extends Payload>[] payload() default {};
+}
diff --git a/core/src/main/java/io/kestra/core/validations/validator/ConditionValidator.java b/core/src/main/java/io/kestra/core/validations/validator/ConditionValidator.java
new file mode 100644
index 000000000..b2e4a1bb4
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/validations/validator/ConditionValidator.java
@@ -0,0 +1,22 @@
+package io.kestra.core.validations.validator;
+
+import io.kestra.core.validations.ConditionValidation;
+import io.kestra.plugin.core.trigger.Flow;
+import io.micronaut.core.annotation.AnnotationValue;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.validation.validator.constraints.ConstraintValidator;
+import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ConditionValidator implements ConstraintValidator<ConditionValidation, Flow> {
+    @Override
+    public boolean isValid(@Nullable Flow value, @NonNull AnnotationValue<ConditionValidation> annotationMetadata, @NonNull ConstraintValidatorContext context) {
+        if (value == null) {
+            return true;
+        }
+
+        return value.getConditions() != null || value.getPreconditions() != null;
+    }
+}
diff --git a/core/src/main/java/io/kestra/plugin/core/trigger/Flow.java b/core/src/main/java/io/kestra/plugin/core/trigger/Flow.java
index 0bb1622bd..2aa9a0cbf 100644
--- a/core/src/main/java/io/kestra/plugin/core/trigger/Flow.java
+++ b/core/src/main/java/io/kestra/plugin/core/trigger/Flow.java
@@ -4,46 +4,43 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.kestra.core.exceptions.IllegalVariableEvaluationException;
 import io.kestra.core.exceptions.InternalException;
 import io.kestra.core.models.Label;
+import io.kestra.core.models.annotations.Example;
+import io.kestra.core.models.annotations.Plugin;
 import io.kestra.core.models.annotations.PluginProperty;
 import io.kestra.core.models.conditions.Condition;
 import io.kestra.core.models.conditions.ConditionContext;
+import io.kestra.core.models.executions.Execution;
+import io.kestra.core.models.executions.ExecutionTrigger;
+import io.kestra.core.models.flows.State;
+import io.kestra.core.models.triggers.AbstractTrigger;
 import io.kestra.core.models.triggers.TimeWindow;
+import io.kestra.core.models.triggers.TriggerOutput;
 import io.kestra.core.models.triggers.multipleflows.MultipleCondition;
+import io.kestra.core.runners.RunContext;
 import io.kestra.core.services.LabelService;
+import io.kestra.core.utils.IdUtils;
 import io.kestra.core.utils.ListUtils;
 import io.kestra.core.utils.TruthUtils;
+import io.kestra.core.validations.ConditionValidation;
 import io.kestra.core.validations.PreconditionFilterValidation;
+import io.micronaut.core.annotation.Nullable;
 import io.swagger.v3.oas.annotations.Hidden;
 import io.swagger.v3.oas.annotations.media.Schema;
-
-import java.util.*;
-
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Pattern;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
-import io.kestra.core.models.annotations.Example;
-import io.kestra.core.models.annotations.Plugin;
-import io.kestra.core.models.executions.Execution;
-import io.kestra.core.models.executions.ExecutionTrigger;
-import io.kestra.core.models.flows.State;
-import io.kestra.core.models.triggers.AbstractTrigger;
-import io.kestra.core.models.triggers.TriggerOutput;
-import io.kestra.core.runners.RunContext;
-import io.kestra.core.utils.IdUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 
-import java.util.List;
+import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import io.micronaut.core.annotation.Nullable;
-import jakarta.validation.constraints.NotNull;
-
 import static io.kestra.core.topologies.FlowTopologyService.SIMULATED_EXECUTION;
 import static io.kestra.core.utils.Rethrow.throwPredicate;
 
@@ -192,6 +189,7 @@ import static io.kestra.core.utils.Rethrow.throwPredicate;
     aliases = "io.kestra.core.models.triggers.types.Flow"
 )
 @Slf4j
+@ConditionValidation
 public class Flow extends AbstractTrigger implements TriggerOutput<Flow.Output> {
     private static final String TRIGGER_VAR = "trigger";
     private static final String OUTPUTS_VAR = "outputs";


Test patch:
diff --git a/core/src/test/java/io/kestra/core/services/FlowServiceTest.java b/core/src/test/java/io/kestra/core/services/FlowServiceTest.java
index 4743fae13..bec7f64b1 100644
--- a/core/src/test/java/io/kestra/core/services/FlowServiceTest.java
+++ b/core/src/test/java/io/kestra/core/services/FlowServiceTest.java
@@ -205,26 +205,6 @@ class FlowServiceTest {
         assertThat(collect.stream().filter(flow -> flow.getId().equals("test3")).findFirst().orElseThrow().getRevision(), is(3));
     }
 
-    @Test
-    void warnings() {
-        Flow flow = create("test", "test", 1).toBuilder()
-            .namespace("system")
-            .triggers(List.of(
-                io.kestra.plugin.core.trigger.Flow.builder()
-                    .id("flow-trigger")
-                    .type(io.kestra.plugin.core.trigger.Flow.class.getName())
-                    .build()
-            ))
-            .build();
-
-        List<String> warnings = flowService.warnings(flow);
-
-        assertThat(warnings.size(), is(1));
-        assertThat(warnings, containsInAnyOrder(
-            "This flow will be triggered for EVERY execution of EVERY flow on your instance. We recommend adding the preconditions property to the Flow trigger 'flow-trigger'."
-        ));
-    }
-
     @Test
     void aliases() {
         List<FlowService.Relocation> warnings = flowService.relocations("""