mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
feat(flows): add new check conditions
Adds new property 'checks' on flow in order to allow pre-conditions to be evaluated before execution Fixes: kestra-io/kestra-ee#5759
This commit is contained in:
committed by
Florian Hussonnois
parent
90ee720d49
commit
eb51c5be37
@@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.annotations.PluginProperty;
|
||||
import io.kestra.core.models.flows.check.Check;
|
||||
import io.kestra.core.models.flows.sla.SLA;
|
||||
import io.kestra.core.models.listeners.Listener;
|
||||
import io.kestra.core.models.tasks.FlowableTask;
|
||||
@@ -129,6 +130,14 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
@Valid
|
||||
@PluginProperty
|
||||
List<SLA> sla;
|
||||
|
||||
@Schema(
|
||||
title = "Conditions evaluated before the flow is executed.",
|
||||
description = "A list of conditions that are evaluated before the flow is executed. If no checks are defined, the flow executes normally."
|
||||
)
|
||||
@Valid
|
||||
@PluginProperty
|
||||
List<Check> checks;
|
||||
|
||||
public Stream<String> allTypes() {
|
||||
return Stream.of(
|
||||
|
||||
@@ -43,6 +43,7 @@ public class FlowWithSource extends Flow {
|
||||
.concurrency(this.concurrency)
|
||||
.retry(this.retry)
|
||||
.sla(this.sla)
|
||||
.checks(this.checks)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ public class FlowWithSource extends Flow {
|
||||
.concurrency(flow.concurrency)
|
||||
.retry(flow.retry)
|
||||
.sla(flow.sla)
|
||||
.checks(flow.checks)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
109
core/src/main/java/io/kestra/core/models/flows/check/Check.java
Normal file
109
core/src/main/java/io/kestra/core/models/flows/check/Check.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package io.kestra.core.models.flows.check;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a check within a Kestra flow.
|
||||
* <p>
|
||||
* A {@code Check} defines a boolean condition that is evaluated when validating flow's inputs
|
||||
* and before triggering an execution.
|
||||
* <p>
|
||||
* If the condition evaluates to {@code false}, the configured {@link Behavior}
|
||||
* determines how the execution proceeds, and the {@link Style} determines how
|
||||
* the message is visually presented in the UI.
|
||||
* </p>
|
||||
*/
|
||||
@SuperBuilder
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class Check {
|
||||
|
||||
/**
|
||||
* The condition to evaluate.
|
||||
*/
|
||||
@NotNull
|
||||
@NotEmpty
|
||||
String condition;
|
||||
|
||||
/**
|
||||
* The message associated with this check, will be displayed when the condition evaluates to {@code false}.
|
||||
*/
|
||||
@NotEmpty
|
||||
String message;
|
||||
|
||||
/**
|
||||
* Defines the style of the message displayed in the UI when the condition evaluates to {@code false}.
|
||||
*/
|
||||
Style style = Style.INFO;
|
||||
|
||||
/**
|
||||
* The behavior to apply when the condition evaluates to {@code false}.
|
||||
*/
|
||||
Behavior behavior = Behavior.BLOCK_EXECUTION;
|
||||
|
||||
/**
|
||||
* The visual style used to display the message when the check fails.
|
||||
*/
|
||||
public enum Style {
|
||||
/**
|
||||
* Display the message as an error.
|
||||
*/
|
||||
ERROR,
|
||||
/**
|
||||
* Display the message as a success indicator.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* Display the message as a warning.
|
||||
*/
|
||||
WARNING,
|
||||
/**
|
||||
* Display the message as informational content.
|
||||
*/
|
||||
INFO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how the flow should behave when the condition evaluates to {@code false}.
|
||||
*/
|
||||
public enum Behavior {
|
||||
/**
|
||||
* Block the creation of the execution.
|
||||
*/
|
||||
BLOCK_EXECUTION,
|
||||
/**
|
||||
* Create the execution as failed.
|
||||
*/
|
||||
FAIL_EXECUTION,
|
||||
/**
|
||||
* Create a new execution as a result of the check failing.
|
||||
*/
|
||||
CREATE_EXECUTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the effective behavior for a list of {@link Check}s based on priority.
|
||||
*
|
||||
* @param checks the list of checks whose behaviors are to be evaluated
|
||||
* @return the highest-priority behavior, or {@code CREATE_EXECUTION} if the list is empty or only contains nulls
|
||||
*/
|
||||
public static Check.Behavior resolveBehavior(List<Check> checks) {
|
||||
if (checks == null || checks.isEmpty()) {
|
||||
return Behavior.CREATE_EXECUTION;
|
||||
}
|
||||
|
||||
return checks.stream()
|
||||
.map(Check::getBehavior)
|
||||
.filter(Objects::nonNull).min(Comparator.comparingInt(Enum::ordinal))
|
||||
.orElse(Behavior.CREATE_EXECUTION);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,20 +2,26 @@ package io.kestra.core.services;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.kestra.core.exceptions.FlowProcessingException;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.*;
|
||||
import io.kestra.core.models.flows.check.Check;
|
||||
import io.kestra.core.models.tasks.RunnableTask;
|
||||
import io.kestra.core.models.topologies.FlowTopology;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.models.validations.ValidateConstraintViolation;
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.repositories.FlowTopologyRepositoryInterface;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.runners.RunContextFactory;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.plugin.core.flow.Pause;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Provider;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -27,6 +33,7 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -53,6 +60,9 @@ public class FlowService {
|
||||
|
||||
@Inject
|
||||
Optional<FlowTopologyRepositoryInterface> flowTopologyRepository;
|
||||
|
||||
@Inject
|
||||
Provider<RunContextFactory> runContextFactory; // Lazy init: avoid circular dependency error.
|
||||
|
||||
/**
|
||||
* Validates and creates the given flow.
|
||||
@@ -84,7 +94,51 @@ public class FlowService {
|
||||
return flowRepository
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot perform operation on flow. Cause: No FlowRepository"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluates all checks defined in the given flow using the provided inputs.
|
||||
* <p>
|
||||
* Each check's {@link Check#getCondition()} is evaluated in the context of the flow.
|
||||
* If a condition evaluates to {@code false} or fails to evaluate due to a
|
||||
* variable error, the corresponding {@link Check} is added to the returned list.
|
||||
* </p>
|
||||
*
|
||||
* @param flow the flow containing the checks to evaluate
|
||||
* @param inputs the input values used when evaluating the conditions
|
||||
* @return a list of checks whose conditions evaluated to {@code false} or failed to evaluate
|
||||
*/
|
||||
public List<Check> getFailedChecks(Flow flow, Map<String, Object> inputs) {
|
||||
if (!ListUtils.isEmpty(flow.getChecks())) {
|
||||
RunContext runContext = runContextFactory.get().of(flow, Map.of("inputs", inputs));
|
||||
List<Check> falseConditions = new ArrayList<>();
|
||||
for (Check check : flow.getChecks()) {
|
||||
try {
|
||||
boolean result = Boolean.TRUE.equals(runContext.renderTyped(check.getCondition()));
|
||||
if (!result) {
|
||||
falseConditions.add(check);
|
||||
}
|
||||
} catch (IllegalVariableEvaluationException e) {
|
||||
log.debug("[tenant: {}] [namespace: {}] [flow: {}] Failed to evaluate check condition. Cause.: {}",
|
||||
flow.getTenantId(),
|
||||
flow.getNamespace(),
|
||||
flow.getId(),
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
falseConditions.add(Check
|
||||
.builder()
|
||||
.message("Failed to evaluate check condition. Cause: " + e.getMessage())
|
||||
.behavior(Check.Behavior.BLOCK_EXECUTION)
|
||||
.style(Check.Style.ERROR)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
return falseConditions;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given flow source.
|
||||
* <p>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package io.kestra.core.models.flows.check;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class CheckTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnCreateExecutionGivenNullList() {
|
||||
// Given
|
||||
List<Check> checks = null;
|
||||
|
||||
// When
|
||||
Check.Behavior result = Check.resolveBehavior(checks);
|
||||
|
||||
// Then
|
||||
assertThat(result).isEqualTo(Check.Behavior.CREATE_EXECUTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCreateExecutionGivenEmptyList() {
|
||||
// Given
|
||||
List<Check> checks = List.of();
|
||||
|
||||
// When
|
||||
Check.Behavior result = Check.resolveBehavior(checks);
|
||||
|
||||
// Then
|
||||
assertThat(result).isEqualTo(Check.Behavior.CREATE_EXECUTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCreateExecutionGivenOnlyCreateExecutionChecks() {
|
||||
// Given
|
||||
List<Check> checks = List.of(
|
||||
Check.builder().behavior(Check.Behavior.CREATE_EXECUTION).build(),
|
||||
Check.builder().behavior(Check.Behavior.CREATE_EXECUTION).build()
|
||||
);
|
||||
|
||||
// When
|
||||
Check.Behavior result = Check.resolveBehavior(checks);
|
||||
|
||||
// Then
|
||||
assertThat(result).isEqualTo(Check.Behavior.CREATE_EXECUTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFailExecutionGivenFailAndCreateChecks() {
|
||||
// Given
|
||||
List<Check> checks = List.of(
|
||||
Check.builder().behavior(Check.Behavior.CREATE_EXECUTION).build(),
|
||||
Check.builder().behavior(Check.Behavior.FAIL_EXECUTION).build()
|
||||
);
|
||||
|
||||
// When
|
||||
Check.Behavior result = Check.resolveBehavior(checks);
|
||||
|
||||
// Then
|
||||
assertThat(result).isEqualTo(Check.Behavior.FAIL_EXECUTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnBlockExecutionGivenMixedBehaviors() {
|
||||
// Given
|
||||
List<Check> checks = List.of(
|
||||
Check.builder().behavior(Check.Behavior.CREATE_EXECUTION).build(),
|
||||
Check.builder().behavior(Check.Behavior.FAIL_EXECUTION).build(),
|
||||
Check.builder().behavior(Check.Behavior.BLOCK_EXECUTION).build()
|
||||
);
|
||||
|
||||
// When
|
||||
Check.Behavior result = Check.resolveBehavior(checks);
|
||||
|
||||
// Then
|
||||
assertThat(result).isEqualTo(Check.Behavior.BLOCK_EXECUTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreNullBehaviorsGivenMixedValues() {
|
||||
// Given
|
||||
List<Check> checks = List.of(
|
||||
Check.builder().behavior(null).build(),
|
||||
Check.builder().behavior(Check.Behavior.CREATE_EXECUTION).build()
|
||||
);
|
||||
|
||||
// When
|
||||
Check.Behavior result = Check.resolveBehavior(checks);
|
||||
|
||||
// Then
|
||||
assertThat(result).isEqualTo(Check.Behavior.CREATE_EXECUTION);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.exceptions.FlowProcessingException;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.models.flows.Type;
|
||||
import io.kestra.core.models.flows.check.Check;
|
||||
import io.kestra.core.models.flows.input.StringInput;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.validations.ValidateConstraintViolation;
|
||||
@@ -17,11 +20,14 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@KestraTest
|
||||
class FlowServiceTest {
|
||||
@@ -431,4 +437,106 @@ class FlowServiceTest {
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.getFirst().getConstraints()).contains("Flow id is a reserved keyword: pause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListGivenFlowWithNoChecks() {
|
||||
// Given
|
||||
Flow flow = mock(Flow.class);
|
||||
when(flow.getChecks()).thenReturn(List.of());
|
||||
|
||||
// When
|
||||
List<Check> result = flowService.getFailedChecks(flow, Map.of());
|
||||
|
||||
// Then
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCheckWhenConditionEvaluatesFalse() {
|
||||
// Given
|
||||
Check failingCheck = Check.builder()
|
||||
.condition("{{ false }}")
|
||||
.message("fail")
|
||||
.behavior(Check.Behavior.FAIL_EXECUTION)
|
||||
.build();
|
||||
Flow flow = mock(Flow.class);
|
||||
when(flow.getChecks()).thenReturn(List.of(failingCheck));
|
||||
when(flow.getNamespace()).thenReturn("io.kestra.unittest");
|
||||
when(flow.getId()).thenReturn("test");
|
||||
|
||||
// When
|
||||
List<Check> result = flowService.getFailedChecks(flow, Map.of());
|
||||
|
||||
// Then
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst()).isEqualTo(failingCheck);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListWhenConditionEvaluatesTrue() {
|
||||
// Given
|
||||
Check passingCheck = Check.builder()
|
||||
.condition("{{ true }}")
|
||||
.message("pass")
|
||||
.behavior(Check.Behavior.FAIL_EXECUTION)
|
||||
.build();
|
||||
Flow flow = mock(Flow.class);
|
||||
when(flow.getChecks()).thenReturn(List.of(passingCheck));
|
||||
when(flow.getNamespace()).thenReturn("io.kestra.unittest");
|
||||
when(flow.getId()).thenReturn("test");
|
||||
|
||||
// When
|
||||
List<Check> result = flowService.getFailedChecks(flow, Map.of());
|
||||
|
||||
// Then
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCheckWithErrorMessageWhenExceptionThrown() {
|
||||
// Given
|
||||
Check check = Check.builder()
|
||||
.condition("{{ invalidFunction() }}")
|
||||
.message("ignored")
|
||||
.behavior(Check.Behavior.FAIL_EXECUTION)
|
||||
.build();
|
||||
Flow flow = mock(Flow.class);
|
||||
when(flow.getChecks()).thenReturn(List.of(check));
|
||||
when(flow.getNamespace()).thenReturn("io.kestra.unittest");
|
||||
when(flow.getId()).thenReturn("test");
|
||||
|
||||
// When
|
||||
List<Check> result = flowService.getFailedChecks(flow, Map.of());
|
||||
|
||||
// Then
|
||||
assertThat(result).hasSize(1);
|
||||
Check errorCheck = result.getFirst();
|
||||
assertThat(errorCheck.getBehavior()).isEqualTo(Check.Behavior.BLOCK_EXECUTION);
|
||||
assertThat(errorCheck.getStyle()).isEqualTo(Check.Style.ERROR);
|
||||
assertThat(errorCheck.getMessage()).contains("Failed to evaluate check condition. Cause:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMultipleChecksWithMixedResults() {
|
||||
// Given
|
||||
Check passCheck = Check.builder().condition("{{ true }}").message("pass").build();
|
||||
Check failCheck = Check.builder().condition("{{ false }}").message("fail").build();
|
||||
Check exceptionCheck = Check.builder().condition("{{ invalidFunction }}").message("exception").build();
|
||||
|
||||
Flow flow = mock(Flow.class);
|
||||
when(flow.getChecks()).thenReturn(List.of(passCheck, failCheck, exceptionCheck));
|
||||
when(flow.getNamespace()).thenReturn("io.kestra.unittest");
|
||||
when(flow.getId()).thenReturn("test");
|
||||
|
||||
// When
|
||||
List<Check> result = flowService.getFailedChecks(flow, Map.of());
|
||||
|
||||
// Then
|
||||
assertThat(result).hasSize(2);
|
||||
assertThat(result).contains(failCheck);
|
||||
assertThat(result)
|
||||
.anyMatch(c -> c.getMessage().contains("Failed to evaluate check condition") &&
|
||||
c.getBehavior() == Check.Behavior.BLOCK_EXECUTION &&
|
||||
c.getStyle() == Check.Style.ERROR);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,22 @@
|
||||
<strong>{{ $t('disabled flow title') }}</strong><br>
|
||||
{{ $t('disabled flow desc') }}
|
||||
</el-alert>
|
||||
|
||||
<div class="flow-execution-checks-alerts">
|
||||
<el-alert v-for="alert in checks || []" :type="alert.style.toLowerCase()" showIcon :closable="false" :key="alert">
|
||||
{{ alert.message }}
|
||||
</el-alert>
|
||||
</div>
|
||||
<el-form labelPosition="top" :model="inputs" ref="form" @submit.prevent="false">
|
||||
<InputsForm :initialInputs="flow.inputs" :selectedTrigger="selectedTrigger" :flow="flow" v-model="inputs" :executeClicked="executeClicked" @confirm="onSubmit($refs.form)" @update:model-value-no-default="values => inputsNoDefaults=values" />
|
||||
<InputsForm
|
||||
:initialInputs="flow.inputs"
|
||||
:selectedTrigger="selectedTrigger"
|
||||
:flow="flow"
|
||||
v-model="inputs"
|
||||
:executeClicked="executeClicked"
|
||||
@confirm="onSubmit($refs.form)"
|
||||
@update:model-value-no-default="values => inputsNoDefaults=values"
|
||||
@update:checks="values => checks=values"
|
||||
/>
|
||||
|
||||
<el-collapse v-model="collapseName">
|
||||
<el-collapse-item :title="$t('advanced configuration')" name="advanced">
|
||||
@@ -48,7 +61,7 @@
|
||||
<el-button
|
||||
:data-test-id="buttonTestId"
|
||||
:icon="buttonIcon"
|
||||
:disabled="!flowCanBeExecuted"
|
||||
:disabled="!flowCanBeExecuted || hasBlockingChecks()"
|
||||
:class="{'flow-run-trigger-button': true, 'onboarding-glow': coreStore.guidedProperties.tourStarted}"
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
@@ -113,6 +126,7 @@
|
||||
collapseName: undefined,
|
||||
newTab: localStorage.getItem(storageKeys.EXECUTE_FLOW_BEHAVIOUR) === executeFlowBehaviours.NEW_TAB,
|
||||
executeClicked: false,
|
||||
checks: []
|
||||
};
|
||||
},
|
||||
emits: ["executionTrigger", "updateInputs", "updateLabels"],
|
||||
@@ -141,6 +155,9 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasBlockingChecks() {
|
||||
return this.checks.filter(check => check.behavior === "BLOCK_EXECUTION").length > 0;
|
||||
},
|
||||
getExecutionLabels() {
|
||||
if (!this.execution.labels) {
|
||||
return [];
|
||||
@@ -243,6 +260,9 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.flow-execution-checks-alerts {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
:deep(.el-collapse) {
|
||||
border-radius: var(--bs-border-radius-lg);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
|
||||
@@ -341,10 +341,10 @@
|
||||
DeleteOutline,
|
||||
Pencil,
|
||||
Plus,
|
||||
ContentSave,
|
||||
ContentSave
|
||||
};
|
||||
},
|
||||
emits: ["update:modelValue", "update:modelValueNoDefault", "confirm", "validation"],
|
||||
emits: ["update:modelValue", "update:modelValueNoDefault", "update:checks", "confirm", "validation"],
|
||||
created() {
|
||||
this.inputsMetaData = JSON.parse(JSON.stringify(this.initialInputs));
|
||||
this.debouncedValidation = debounce(this.validateInputs, 500)
|
||||
@@ -391,6 +391,7 @@
|
||||
document.removeEventListener("keydown", this._keyListener);
|
||||
},
|
||||
methods: {
|
||||
|
||||
normalizeJSON(value) {
|
||||
try {
|
||||
// Step 1: Remove trailing commas in objects and arrays
|
||||
@@ -537,6 +538,7 @@
|
||||
const formData = inputsToFormData(this, this.inputsMetaData, inputsValuesWithNoDefault);
|
||||
|
||||
const metadataCallback = (response) => {
|
||||
this.$emit("update:checks", response.checks || []);
|
||||
this.inputsMetaData = response.inputs.reduce((acc,it) => {
|
||||
if(it.enabled){
|
||||
acc.push({...it.input, errors: it.errors, value: it.value || it.input.prefill, isDefault: it.isDefault});
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.executions.*;
|
||||
import io.kestra.core.models.flows.*;
|
||||
import io.kestra.core.models.flows.check.Check;
|
||||
import io.kestra.core.models.flows.input.InputAndValue;
|
||||
import io.kestra.core.models.hierarchies.FlowGraph;
|
||||
import io.kestra.core.models.storage.FileMetas;
|
||||
@@ -91,6 +92,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.event.Level;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -190,14 +192,14 @@ public class ExecutionController {
|
||||
@Inject
|
||||
private Optional<OpenTelemetry> openTelemetry;
|
||||
|
||||
@Inject
|
||||
private ExecutionStreamingService executionStreamingService;
|
||||
|
||||
@Inject
|
||||
private LocalPathFactory localPathFactory;
|
||||
|
||||
@Inject
|
||||
private SecureVariableRendererFactory secureVariableRendererFactory;
|
||||
|
||||
@Inject
|
||||
private LogService logService;
|
||||
|
||||
@Value("${" + LocalPath.ENABLE_PREVIEW_CONFIG + ":true}")
|
||||
private boolean enableLocalFilePreview;
|
||||
@@ -691,7 +693,11 @@ public class ExecutionController {
|
||||
Execution execution = Execution.newExecution(flow, parsedLabels);
|
||||
return flowInputOutput
|
||||
.validateExecutionInputs(flow.getInputs(), flow, execution, inputs)
|
||||
.map(values -> ApiValidateExecutionInputsResponse.of(id, namespace, values));
|
||||
.map(values -> {
|
||||
Map<String, Object> inputsAsMap = values.stream().collect(HashMap::new, (m,v)->m.put(v.input().getId(), v.value()), HashMap::putAll);
|
||||
List<Check> checks = flowService.getFailedChecks(flow, inputsAsMap);
|
||||
return ApiValidateExecutionInputsResponse.of(id, namespace, checks, values);
|
||||
});
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@@ -729,7 +735,24 @@ public class ExecutionController {
|
||||
|
||||
return flowInputOutput.readExecutionInputs(flow, current, inputs)
|
||||
.flatMap(executionInputs -> {
|
||||
Execution executionWithInputs = current.withInputs(executionInputs);
|
||||
Check.Behavior behavior = Check.resolveBehavior(flowService.getFailedChecks(flow, executionInputs));
|
||||
if (Check.Behavior.BLOCK_EXECUTION.equals(behavior)) {
|
||||
return Mono.error(new IllegalArgumentException(
|
||||
"Flow execution blocked: one or more condition checks evaluated to false."
|
||||
));
|
||||
}
|
||||
|
||||
final Execution executionWithInputs = Optional.of(current.withInputs(executionInputs))
|
||||
.map(exec -> {
|
||||
if (Check.Behavior.FAIL_EXECUTION.equals(behavior)) {
|
||||
this.logService.logExecution(current, log, Level.WARN, "Flow execution failed because one or more condition checks evaluated to false.");
|
||||
return exec.withState(State.Type.FAILED);
|
||||
} else {
|
||||
return exec;
|
||||
}
|
||||
})
|
||||
.get();
|
||||
|
||||
try {
|
||||
// inject the traceparent into the execution
|
||||
openTelemetry
|
||||
@@ -740,7 +763,7 @@ public class ExecutionController {
|
||||
executionQueue.emit(executionWithInputs);
|
||||
eventPublisher.publishEvent(new CrudEvent<>(executionWithInputs, CrudEventType.CREATE));
|
||||
|
||||
if (!wait) {
|
||||
if (!wait || executionWithInputs.getState().isFailed()) {
|
||||
return Mono.just(ExecutionResponse.fromExecution(
|
||||
executionWithInputs,
|
||||
executionUrl(executionWithInputs)
|
||||
@@ -1457,7 +1480,7 @@ public class ExecutionController {
|
||||
Flow flow = flowRepository.findByExecutionWithoutAcl(execution);
|
||||
|
||||
return executionService.validateForResume(execution, flow, inputs)
|
||||
.map(values -> ApiValidateExecutionInputsResponse.of(execution.getFlowId(), execution.getNamespace(), values))
|
||||
.map(values -> ApiValidateExecutionInputsResponse.of(execution.getFlowId(), execution.getNamespace(), List.of(), values))
|
||||
// need to consume the inputs in case of error
|
||||
.doOnError(t -> Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast());
|
||||
}
|
||||
@@ -2574,7 +2597,8 @@ public class ExecutionController {
|
||||
@Parameter(description = "The namespace")
|
||||
String namespace,
|
||||
@Parameter(description = "The flow's inputs")
|
||||
List<ApiInputAndValue> inputs
|
||||
List<ApiInputAndValue> inputs,
|
||||
List<ApiCheckFailure> checks
|
||||
) {
|
||||
|
||||
@Introspected
|
||||
@@ -2597,10 +2621,20 @@ public class ExecutionController {
|
||||
@Parameter(description = "The error message")
|
||||
String message
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@Introspected
|
||||
public record ApiCheckFailure(
|
||||
@Parameter(description = "The message")
|
||||
String message,
|
||||
@Parameter(description = "The message style")
|
||||
Check.Style style,
|
||||
@Parameter(description = "The behavior")
|
||||
Check.Behavior behavior
|
||||
) {
|
||||
}
|
||||
|
||||
public static ApiValidateExecutionInputsResponse of(String id, String namespace, List<InputAndValue> inputs) {
|
||||
public static ApiValidateExecutionInputsResponse of(String id, String namespace, List<Check> checks, List<InputAndValue> inputs) {
|
||||
return new ApiValidateExecutionInputsResponse(
|
||||
id,
|
||||
namespace,
|
||||
@@ -2615,7 +2649,8 @@ public class ExecutionController {
|
||||
.map(cv -> new ApiInputError(cv.getMessage()))
|
||||
.toList()
|
||||
).orElse(List.of())
|
||||
)).toList()
|
||||
)).toList(),
|
||||
checks.stream().map(check -> new ApiCheckFailure(check.getMessage(), check.getStyle(), check.getBehavior())).toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user