Compare commits

..

1 Commits

Author SHA1 Message Date
Roman Acevedo
55458a0428 test: add test on executor 2025-09-24 08:23:17 +02:00
49 changed files with 489 additions and 542 deletions

View File

@@ -37,7 +37,7 @@ plugins {
id "com.vanniktech.maven.publish" version "0.34.0"
// OWASP dependency check
id "org.owasp.dependencycheck" version "12.1.5" apply false
id "org.owasp.dependencycheck" version "12.1.3" apply false
}
idea {

View File

@@ -2,27 +2,19 @@ package io.kestra.cli.commands.servers;
import io.kestra.cli.AbstractCommand;
import io.kestra.core.contexts.KestraContext;
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.PostConstruct;
import picocli.CommandLine;
@Slf4j
public abstract class AbstractServerCommand extends AbstractCommand implements ServerCommandInterface {
abstract public class AbstractServerCommand extends AbstractCommand implements ServerCommandInterface {
@CommandLine.Option(names = {"--port"}, description = "The port to bind")
Integer serverPort;
@Override
public Integer call() throws Exception {
log.info("Machine information: {} available cpu(s), {}MB max memory, Java version {}", Runtime.getRuntime().availableProcessors(), maxMemoryInMB(), Runtime.version());
this.shutdownHook(true, () -> KestraContext.getContext().shutdown());
return super.call();
}
private long maxMemoryInMB() {
return Runtime.getRuntime().maxMemory() / 1024 / 1024;
}
protected static int defaultWorkerThread() {
return Runtime.getRuntime().availableProcessors() * 8;
}

View File

@@ -18,7 +18,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junitpioneer.jupiter.RetryingTest;
import static io.kestra.core.utils.Rethrow.throwRunnable;
import static org.assertj.core.api.Assertions.assertThat;
@@ -95,7 +94,7 @@ class FileChangedEventListenerTest {
);
}
@RetryingTest(2)
@Test
void testWithPluginDefault() throws IOException, TimeoutException {
var tenant = TestsUtils.randomTenant(FileChangedEventListenerTest.class.getName(), "testWithPluginDefault");
// remove the flow if it already exists

View File

@@ -84,7 +84,7 @@ dependencies {
testImplementation "org.testcontainers:testcontainers:1.21.3"
testImplementation "org.testcontainers:junit-jupiter:1.21.3"
testImplementation "org.bouncycastle:bcpkix-jdk18on"
testImplementation "org.bouncycastle:bcpkix-jdk18on:1.81"
testImplementation "org.wiremock:wiremock-jetty12"
}

View File

@@ -2,13 +2,12 @@ package io.kestra.core.models;
import io.kestra.core.utils.MapUtils;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public record Label(@NotEmpty String key, @NotEmpty String value) {
public record Label(@NotNull String key, @NotNull String value) {
public static final String SYSTEM_PREFIX = "system.";
// system labels
@@ -42,7 +41,7 @@ public record Label(@NotEmpty String key, @NotEmpty String value) {
public static Map<String, String> toMap(@Nullable List<Label> labels) {
if (labels == null || labels.isEmpty()) return Collections.emptyMap();
return labels.stream()
.filter(label -> label.value() != null && !label.value().isEmpty() && label.key() != null && !label.key().isEmpty())
.filter(label -> label.value() != null && label.key() != null)
// using an accumulator in case labels with the same key exists: the second is kept
.collect(Collectors.toMap(Label::key, Label::value, (first, second) -> second, LinkedHashMap::new));
}
@@ -57,7 +56,6 @@ public record Label(@NotEmpty String key, @NotEmpty String value) {
public static List<Label> deduplicate(@Nullable List<Label> labels) {
if (labels == null || labels.isEmpty()) return Collections.emptyList();
return toMap(labels).entrySet().stream()
.filter(getEntryNotEmptyPredicate())
.map(entry -> new Label(entry.getKey(), entry.getValue()))
.collect(Collectors.toCollection(ArrayList::new));
}
@@ -72,7 +70,6 @@ public record Label(@NotEmpty String key, @NotEmpty String value) {
if (map == null || map.isEmpty()) return List.of();
return map.entrySet()
.stream()
.filter(getEntryNotEmptyPredicate())
.map(entry -> new Label(entry.getKey(), entry.getValue()))
.toList();
}
@@ -91,14 +88,4 @@ public record Label(@NotEmpty String key, @NotEmpty String value) {
}
return map;
}
/**
* Provides predicate for not empty entries.
*
* @return The non-empty filter
*/
public static Predicate<Map.Entry<String, String>> getEntryNotEmptyPredicate() {
return entry -> entry.getKey() != null && !entry.getKey().isEmpty() &&
entry.getValue() != null && !entry.getValue().isEmpty();
}
}

View File

@@ -865,18 +865,20 @@ public class Execution implements DeletedInterface, TenantInterface {
* @param e the exception raise
* @return new taskRun with updated attempt with logs
*/
private FailedTaskRunWithLog lastAttemptsTaskRunForFailedExecution(TaskRun taskRun, TaskRunAttempt lastAttempt, Exception e) {
TaskRun failed = taskRun
.withAttempts(
Stream
.concat(
taskRun.getAttempts().stream().limit(taskRun.getAttempts().size() - 1),
Stream.of(lastAttempt.getState().isFailed() ? lastAttempt : lastAttempt.withState(State.Type.FAILED))
)
.toList()
);
private FailedTaskRunWithLog lastAttemptsTaskRunForFailedExecution(TaskRun taskRun,
TaskRunAttempt lastAttempt, Exception e) {
return new FailedTaskRunWithLog(
failed.getState().isFailed() ? failed : failed.withState(State.Type.FAILED),
taskRun
.withAttempts(
Stream
.concat(
taskRun.getAttempts().stream().limit(taskRun.getAttempts().size() - 1),
Stream.of(lastAttempt
.withState(State.Type.FAILED))
)
.toList()
)
.withState(State.Type.FAILED),
RunContextLogger.logEntries(loggingEventFromException(e), LogEntry.of(taskRun, kind))
);
}

View File

@@ -62,7 +62,6 @@ public abstract class AbstractFlow implements FlowInterface {
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
@Schema(implementation = Object.class, oneOf = {List.class, Map.class})
@Valid
List<Label> labels;
@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE)

View File

@@ -5,7 +5,10 @@ import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.models.Label;
import io.kestra.core.models.executions.*;
import io.kestra.core.models.flows.*;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.FlowWithException;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.ExecutableTask;
import io.kestra.core.models.tasks.Task;
@@ -26,7 +29,6 @@ import org.apache.commons.lang3.stream.Streams;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static io.kestra.core.trace.Tracer.throwCallable;
import static io.kestra.core.utils.Rethrow.throwConsumer;
@@ -151,24 +153,17 @@ public final class ExecutableUtils {
currentFlow.getNamespace(),
currentFlow.getId()
)
.orElseThrow(() -> {
String msg = "Unable to find flow '" + subflowNamespace + "'.'" + subflowId + "' with revision '" + subflowRevision.orElse(0) + "'";
runContext.logger().error(msg);
return new IllegalStateException(msg);
});
.orElseThrow(() -> new IllegalStateException("Unable to find flow '" + subflowNamespace + "'.'" + subflowId + "' with revision '" + subflowRevision.orElse(0) + "'"));
if (flow.isDisabled()) {
String msg = "Cannot execute a flow which is disabled";
runContext.logger().error(msg);
throw new IllegalStateException(msg);
throw new IllegalStateException("Cannot execute a flow which is disabled");
}
if (flow instanceof FlowWithException fwe) {
String msg = "Cannot execute an invalid flow: " + fwe.getException();
runContext.logger().error(msg);
throw new IllegalStateException(msg);
throw new IllegalStateException("Cannot execute an invalid flow: " + fwe.getException());
}
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
if (labels != null) {
labels.forEach(throwConsumer(label -> newLabels.add(new Label(runContext.render(label.key()), runContext.render(label.value())))));
}
@@ -206,20 +201,7 @@ public final class ExecutableUtils {
.build()
)
.withScheduleDate(scheduleOnDate);
if(execution.getInputs().size()<inputs.size()) {
Map<String,Object>resolvedInputs=execution.getInputs();
for (var inputKey : inputs.keySet()) {
if (!resolvedInputs.containsKey(inputKey)) {
runContext.logger().warn(
"Input {} was provided by parent execution {} for subflow {}.{} but isn't declared at the subflow inputs",
inputKey,
currentExecution.getId(),
currentTask.subflowId().namespace(),
currentTask.subflowId().flowId()
);
}
}
}
// inject the traceparent into the new execution
propagator.ifPresent(pg -> pg.inject(Context.current(), execution, ExecutionTextMapSetter.INSTANCE));

View File

@@ -49,7 +49,15 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.*;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
@@ -223,19 +231,6 @@ public class FlowInputOutput {
return new AbstractMap.SimpleEntry<>(it.input().getId(), it.value());
})
.collect(HashMap::new, (m,v)-> m.put(v.getKey(), v.getValue()), HashMap::putAll);
if (resolved.size() < data.size()) {
RunContext runContext = runContextFactory.of(flow, execution);
for (var inputKey : data.keySet()) {
if (!resolved.containsKey(inputKey)) {
runContext.logger().warn(
"Input {} was provided for workflow {}.{} but isn't declared in the workflow inputs",
inputKey,
flow.getNamespace(),
flow.getId()
);
}
}
}
return MapUtils.flattenToNestedMap(resolved);
}
@@ -318,15 +313,15 @@ public class FlowInputOutput {
});
resolvable.setInput(input);
Object value = resolvable.get().value();
// resolve default if needed
if (value == null && input.getDefaults() != null) {
value = resolveDefaultValue(input, runContext);
resolvable.isDefault(true);
}
// validate and parse input value
if (value == null) {
if (input.getRequired()) {
@@ -355,7 +350,7 @@ public class FlowInputOutput {
return resolvable.get();
}
public static Object resolveDefaultValue(Input<?> input, PropertyContext renderer) throws IllegalVariableEvaluationException {
return switch (input.getType()) {
case STRING, ENUM, SELECT, SECRET, EMAIL -> resolveDefaultPropertyAs(input, renderer, String.class);
@@ -372,7 +367,7 @@ public class FlowInputOutput {
case MULTISELECT -> resolveDefaultPropertyAsList(input, renderer, String.class);
};
}
@SuppressWarnings("unchecked")
private static <T> Object resolveDefaultPropertyAs(Input<?> input, PropertyContext renderer, Class<T> clazz) throws IllegalVariableEvaluationException {
return Property.as((Property<T>) input.getDefaults(), renderer, clazz);
@@ -381,7 +376,7 @@ public class FlowInputOutput {
private static <T> Object resolveDefaultPropertyAsList(Input<?> input, PropertyContext renderer, Class<T> clazz) throws IllegalVariableEvaluationException {
return Property.asList((Property<List<T>>) input.getDefaults(), renderer, clazz);
}
private RunContext buildRunContextForExecutionAndInputs(final FlowInterface flow, final Execution execution, Map<String, InputAndValue> dependencies) {
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
.stream()
@@ -458,7 +453,7 @@ public class FlowInputOutput {
if (data.getType() == null) {
return Optional.of(new AbstractMap.SimpleEntry<>(data.getId(), current));
}
final Type elementType = data instanceof ItemTypeInterface itemTypeInterface ? itemTypeInterface.getItemType() : null;
return Optional.of(new AbstractMap.SimpleEntry<>(
@@ -535,17 +530,17 @@ public class FlowInputOutput {
throw new Exception("Expected `" + type + "` but received `" + current + "` with errors:\n```\n" + e.getMessage() + "\n```");
}
}
public static Map<String, Object> renderFlowOutputs(List<Output> outputs, RunContext runContext) throws IllegalVariableEvaluationException {
if (outputs == null) return Map.of();
// render required outputs
Map<String, Object> outputsById = outputs
.stream()
.filter(output -> output.getRequired() == null || output.getRequired())
.collect(HashMap::new, (map, entry) -> map.put(entry.getId(), entry.getValue()), Map::putAll);
outputsById = runContext.render(outputsById);
// render optional outputs one by one to catch, log, and skip any error.
for (io.kestra.core.models.flows.Output output : outputs) {
if (Boolean.FALSE.equals(output.getRequired())) {
@@ -588,9 +583,9 @@ public class FlowInputOutput {
}
public void isDefault(boolean isDefault) {
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), isDefault, this.input.exception());
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), isDefault, this.input.exception());
}
public void setInput(final Input<?> input) {
this.input = new InputAndValue(input, this.input.value(), this.input.enabled(), this.input.isDefault(), this.input.exception());
}

View File

@@ -500,7 +500,7 @@ public class FlowableUtils {
ArrayList<ResolvedTask> result = new ArrayList<>();
int iteration = 0;
int index = 0;
for (Object current : distinctValue) {
try {
String resolvedValue = current instanceof String stringValue ? stringValue : MAPPER.writeValueAsString(current);
@@ -508,7 +508,7 @@ public class FlowableUtils {
result.add(ResolvedTask.builder()
.task(task)
.value(resolvedValue)
.iteration(iteration)
.iteration(index++)
.parentId(parentTaskRun.getId())
.build()
);
@@ -516,7 +516,6 @@ public class FlowableUtils {
} catch (JsonProcessingException e) {
throw new IllegalVariableEvaluationException(e);
}
iteration++;
}
return result;

View File

@@ -30,6 +30,6 @@ public class TimestampMicroFilter extends AbstractDate implements Filter {
ZoneId zoneId = zoneId(timeZone);
ZonedDateTime date = convert(input, zoneId, existingFormat);
return String.valueOf(TimeUnit.SECONDS.toMicros(date.toEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(date.getNano()));
return String.valueOf(TimeUnit.SECONDS.toNanos(date.toEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(date.getNano()));
}
}

View File

@@ -155,7 +155,6 @@ public class Labels extends Task implements ExecutionUpdatableTask {
newLabels.putAll(labelsAsMap);
return execution.withLabels(newLabels.entrySet().stream()
.filter(Label.getEntryNotEmptyPredicate())
.map(entry -> new Label(
entry.getKey(),
entry.getValue()

View File

@@ -1,32 +1,19 @@
package io.kestra.core.models;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.validations.ModelValidator;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest
class LabelTest {
@Inject
private ModelValidator modelValidator;
@Test
void shouldGetNestedMapGivenDistinctLabels() {
Map<String, Object> result = Label.toNestedMap(List.of(
new Label(Label.USERNAME, "test"),
new Label(Label.CORRELATION_ID, "id"),
new Label("", "bar"),
new Label(null, "bar"),
new Label("foo", ""),
new Label("baz", null)
)
new Label(Label.CORRELATION_ID, "id"))
);
assertThat(result).isEqualTo(
@@ -47,18 +34,6 @@ class LabelTest {
);
}
@Test
void toNestedMapShouldIgnoreEmptyOrNull() {
Map<String, Object> result = Label.toNestedMap(List.of(
new Label("", "bar"),
new Label(null, "bar"),
new Label("foo", ""),
new Label("baz", null))
);
assertThat(result).isEmpty();
}
@Test
void shouldGetMapGivenDistinctLabels() {
Map<String, String> result = Label.toMap(List.of(
@@ -84,18 +59,6 @@ class LabelTest {
);
}
@Test
void toMapShouldIgnoreEmptyOrNull() {
Map<String, String> result = Label.toMap(List.of(
new Label("", "bar"),
new Label(null, "bar"),
new Label("foo", ""),
new Label("baz", null))
);
assertThat(result).isEmpty();
}
@Test
void shouldDuplicateLabelsWithKeyOrderKept() {
List<Label> result = Label.deduplicate(List.of(
@@ -110,28 +73,4 @@ class LabelTest {
new Label(Label.CORRELATION_ID, "id")
);
}
@Test
void deduplicateShouldIgnoreEmptyAndNull() {
List<Label> result = Label.deduplicate(List.of(
new Label("", "bar"),
new Label(null, "bar"),
new Label("foo", ""),
new Label("baz", null))
);
assertThat(result).isEmpty();
}
@Test
void shouldValidateEmpty() {
Optional<ConstraintViolationException> validLabelResult = modelValidator.isValid(new Label("foo", "bar"));
assertThat(validLabelResult.isPresent()).isFalse();
Optional<ConstraintViolationException> emptyValueLabelResult = modelValidator.isValid(new Label("foo", ""));
assertThat(emptyValueLabelResult.isPresent()).isTrue();
Optional<ConstraintViolationException> emptyKeyLabelResult = modelValidator.isValid(new Label("", "bar"));
assertThat(emptyKeyLabelResult.isPresent()).isTrue();
}
}

View File

@@ -0,0 +1,15 @@
package io.kestra.core.runners;
import io.kestra.core.junit.annotations.KestraTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.TestInstance;
@KestraTest(startRunner = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class AbstractExecutorTest {
@Inject
protected TestRunnerUtils runnerUtils;
}

View File

@@ -50,7 +50,7 @@ public abstract class AbstractRunnerTest {
private PluginDefaultsCaseTest pluginDefaultsCaseTest;
@Inject
protected FlowCaseTest flowCaseTest;
private FlowCaseTest flowCaseTest;
@Inject
private WorkingDirectoryTest.Suite workingDirectoryTest;
@@ -173,7 +173,7 @@ public abstract class AbstractRunnerTest {
@Test
@LoadFlows({"flows/valids/restart_local_errors.yaml"})
protected void restartFailedThenFailureWithLocalErrors() throws Exception {
void restartFailedThenFailureWithLocalErrors() throws Exception {
restartCaseTest.restartFailedThenFailureWithLocalErrors();
}

View File

@@ -88,7 +88,7 @@ public class FlowTriggerCaseTest {
public void triggerWithConcurrencyLimit(String tenantId) throws QueueException, TimeoutException {
Execution execution1 = runnerUtils.runOneUntilRunning(tenantId, "io.kestra.tests.trigger.concurrency", "trigger-flow-with-concurrency-limit");
Execution execution2 = runnerUtils.runOne(tenantId, "io.kestra.tests.trigger.concurrency", "trigger-flow-with-concurrency-limit");
Execution execution2 = runnerUtils.runOneUntilRunning(tenantId, "io.kestra.tests.trigger.concurrency", "trigger-flow-with-concurrency-limit");
List<Execution> triggeredExec = runnerUtils.awaitFlowExecutionNumber(
5,

View File

@@ -6,8 +6,6 @@ import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.State;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest(startRunner = true)
@@ -33,15 +31,4 @@ class TaskWithRunIfTest {
assertThat(execution.findTaskRunsByTaskId("log_test").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
}
@Test
@ExecuteFlow("flows/valids/task-runif-executionupdating.yml")
void executionUpdatingTask(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.getTaskRunList()).hasSize(5);
assertThat(execution.findTaskRunsByTaskId("skipSetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
assertThat(execution.findTaskRunsByTaskId("skipUnsetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
assertThat(execution.findTaskRunsByTaskId("unsetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.findTaskRunsByTaskId("setVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.getVariables()).containsEntry("list", List.of(42));
}
}

View File

@@ -147,7 +147,7 @@ class DateFilterTest {
)
);
assertThat(render).isEqualTo("1378653552123456");
assertThat(render).isEqualTo("1378653552000123456");
}
@Test

View File

@@ -2,16 +2,12 @@ package io.kestra.plugin.core.flow;
import static org.assertj.core.api.Assertions.assertThat;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.junit.annotations.ExecuteFlow;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.flows.State;
import org.junit.jupiter.api.Test;
import java.util.List;
@KestraTest(startRunner = true)
class ForEachTest {
@@ -64,13 +60,4 @@ class ForEachTest {
void nested(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
}
@Test
@ExecuteFlow("flows/valids/foreach-iteration.yaml")
void iteration(Execution execution) throws InternalException {
List<TaskRun> seconds = execution.findTaskRunsByTaskId("second");
assertThat(seconds).hasSize(2);
assertThat(seconds.get(0).getIteration()).isEqualTo(0);
assertThat(seconds.get(1).getIteration()).isEqualTo(1);
}
}

View File

@@ -160,25 +160,4 @@ class RuntimeLabelsTest {
new Label("fromListKey", "value2")
);
}
@Test
@LoadFlows({"flows/valids/labels-update-task-empty.yml"})
void updateIgnoresEmpty() throws TimeoutException, QueueException {
Execution execution = runnerUtils.runOne(
MAIN_TENANT,
"io.kestra.tests",
"labels-update-task-empty",
null,
(flow, createdExecution) -> Map.of(),
null,
List.of()
);
assertThat(execution.getTaskRunList()).hasSize(1);
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.FAILED);
assertThat(execution.getLabels()).containsExactly(
new Label(Label.CORRELATION_ID, execution.getId())
);
}
}

View File

@@ -168,12 +168,12 @@ class FlowTest {
Optional<Execution> evaluate = flowTrigger.evaluate(multipleConditionStorage, runContextFactory.of(), flow, execution);
assertThat(evaluate.isPresent()).isTrue();
assertThat(evaluate.get().getLabels()).hasSize(5);
assertThat(evaluate.get().getLabels()).hasSize(6);
assertThat(evaluate.get().getLabels()).contains(new Label("flow-label-1", "flow-label-1"));
assertThat(evaluate.get().getLabels()).contains(new Label("flow-label-2", "flow-label-2"));
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-1", "trigger-label-1"));
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-2", "trigger-label-2"));
assertThat(evaluate.get().getLabels()).doesNotContain(new Label("trigger-label-3", ""));
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-3", ""));
assertThat(evaluate.get().getLabels()).contains(new Label(Label.CORRELATION_ID, "correlationId"));
assertThat(evaluate.get().getTrigger()).extracting(ExecutionTrigger::getVariables).hasFieldOrProperty("executionLabels");
assertThat(evaluate.get().getTrigger().getVariables().get("executionLabels")).isEqualTo(Map.of("execution-label", "execution"));

View File

@@ -169,7 +169,7 @@ class ScheduleTest {
assertThat(evaluate.isPresent()).isTrue();
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-1", "trigger-label-1"));
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-2", "trigger-label-2"));
assertThat(evaluate.get().getLabels()).doesNotContain(new Label("trigger-label-3", ""));
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-3", ""));
}
@Test

View File

@@ -1,16 +0,0 @@
id: foreach-iteration
namespace: io.kestra.tests
tasks:
- id: foreach
type: io.kestra.plugin.core.flow.ForEach
values: [1, 2]
tasks:
- id: first
type: io.kestra.plugin.core.output.OutputValues
values:
iteration : "{{ taskrun.iteration }}"
- id: second
type: io.kestra.plugin.core.output.OutputValues
values:
iteration : "{{ taskrun.iteration }}"

View File

@@ -1,14 +0,0 @@
id: labels-update-task-empty
namespace: io.kestra.tests
tasks:
- id: from-string
type: io.kestra.plugin.core.execution.Labels
labels: "{ \"fromStringKey\": \"\", \"\": \"value2\" }"
- id: from-list
type: io.kestra.plugin.core.execution.Labels
labels:
- key: "fromListKey"
value: ""
- key: ""
value: "value2"

View File

@@ -1,35 +0,0 @@
id: task-runif-executionupdating
namespace: io.kestra.tests
variables:
list: []
tasks:
- id: output
type: io.kestra.plugin.core.output.OutputValues
values:
taskrun_data: 1
- id: unsetVariables
type: io.kestra.plugin.core.execution.UnsetVariables
runIf: "true"
variables:
- list
- id: setVariables
type: io.kestra.plugin.core.execution.SetVariables
runIf: "{{ outputs.output['values']['taskrun_data'] == 1 }}"
variables:
list: [42]
- id: skipSetVariables
type: io.kestra.plugin.core.execution.SetVariables
runIf: "false"
variables:
list: [1]
- id: skipUnsetVariables
type: io.kestra.plugin.core.execution.UnsetVariables
runIf: "{{ outputs.output['values']['taskrun_data'] == 2 }}"
variables:
- list

View File

@@ -9,9 +9,6 @@ tasks:
- id: sleep
type: io.kestra.plugin.core.flow.Sleep
duration: PT0.5S
- id: log
type: io.kestra.plugin.core.log.Log
message: "we are between sleeps"
- id: sleep_2
- id: sleep_1
type: io.kestra.plugin.core.flow.Sleep
duration: PT0.5S

View File

@@ -1072,17 +1072,6 @@ public class ExecutorService {
var executionUpdatingTask = (ExecutionUpdatableTask) workerTask.getTask();
try {
// handle runIf
if (!TruthUtils.isTruthy(workerTask.getRunContext().render(workerTask.getTask().getRunIf()))) {
executor.withExecution(
executor
.getExecution()
.withTaskRun(workerTask.getTaskRun().withState(State.Type.SKIPPED)),
"handleExecutionUpdatingTaskSkipped"
);
return false;
}
executor.withExecution(
executionUpdatingTask.update(executor.getExecution(), workerTask.getRunContext())
.withTaskRun(workerTask.getTaskRun().withState(State.Type.RUNNING)),

View File

@@ -0,0 +1,7 @@
package io.kestra.runner.mysql;
import io.kestra.jdbc.runner.JdbcExecutorTest;
public class MysqlExecutorTest extends JdbcExecutorTest {
}

View File

@@ -1,33 +1,7 @@
package io.kestra.runner.mysql;
import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.jdbc.runner.JdbcRunnerTest;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class MysqlRunnerTest extends JdbcRunnerTest {
@Disabled("We have a bug here in the queue where no FAILED event is sent, so the state store is not cleaned")
@Test
@Override
@LoadFlows({"flows/valids/restart-with-finally.yaml"})
protected void restartFailedWithFinally() throws Exception {
restartCaseTest.restartFailedWithFinally();
}
@Disabled("Should fail the second time, but is success")
@Test
@Override
@LoadFlows({"flows/valids/restart_local_errors.yaml"})
protected void restartFailedThenFailureWithLocalErrors() throws Exception {
restartCaseTest.restartFailedThenFailureWithLocalErrors();
}
@Disabled("Is success, but is not terminated")
@Test
@Override
@LoadFlows({"flows/valids/restart-with-after-execution.yaml"})
protected void restartFailedWithAfterExecution() throws Exception {
restartCaseTest.restartFailedWithAfterExecution();
}
}

View File

@@ -513,7 +513,7 @@ public class JdbcExecutor implements ExecutorInterface {
});
}
private void executionQueue(Either<Execution, DeserializationException> either) {
public void executionQueue(Either<Execution, DeserializationException> either) {
if (either.isRight()) {
log.error("Unable to deserialize an execution: {}", either.getRight().getMessage());
return;
@@ -682,8 +682,9 @@ public class JdbcExecutor implements ExecutorInterface {
);
} catch (QueueException e) {
try {
Execution failedExecution = fail(message, e);
this.executionQueue.emit(failedExecution);
this.executionQueue.emit(
message.failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED)
);
} catch (QueueException ex) {
log.error("Unable to emit the execution {}", message.getId(), ex);
}
@@ -700,16 +701,6 @@ public class JdbcExecutor implements ExecutorInterface {
}
}
private Execution fail(Execution message, Exception e) {
var failedExecution = message.failedExecutionFromExecutor(e);
try {
logQueue.emitAsync(failedExecution.getLogs());
} catch (QueueException ex) {
// fail silently
}
return failedExecution.getExecution().getState().isFailed() ? failedExecution.getExecution() : failedExecution.getExecution().withState(State.Type.FAILED);
}
private void workerTaskResultQueue(Either<WorkerTaskResult, DeserializationException> either) {
if (either.isRight()) {
log.error("Unable to deserialize a worker task result: {}", either.getRight().getMessage(), either.getRight());
@@ -1187,9 +1178,8 @@ public class JdbcExecutor implements ExecutorInterface {
// If we cannot add the new worker task result to the execution, we fail it
executionRepository.lock(executor.getExecution().getId(), pair -> {
Execution execution = pair.getLeft();
Execution failedExecution = fail(execution, e);
try {
this.executionQueue.emit(failedExecution);
this.executionQueue.emit(execution.failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED));
} catch (QueueException ex) {
log.error("Unable to emit the execution {}", execution.getId(), ex);
}

View File

@@ -0,0 +1,278 @@
package io.kestra.jdbc.runner;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.runners.AbstractExecutorTest;
import io.kestra.core.runners.AbstractRunnerTest;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.utils.Await;
import io.kestra.core.utils.Either;
import io.kestra.jdbc.JdbcTestUtils;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public abstract class JdbcExecutorTest extends AbstractExecutorTest {
public static final String NAMESPACE = "io.kestra.tests";
@Inject
private JdbcTestUtils jdbcTestUtils;
@BeforeAll
public void init() {
jdbcTestUtils.drop();
jdbcTestUtils.migrate();
}
@Inject
protected JdbcExecutor jdbcExecutor;
@Inject
private ExecutionRepositoryInterface executionRepository;
/**
* {@link AbstractRunnerTest#restartFailedWithFinally()} often fails on Mysql
* this tests tries to reproduce the bug in a simpler context
* this execution is one that was generated by this test when it failed
*/
@Test
@LoadFlows("flows/valids/restart-with-finally.yaml")
void debugRestartFailedWithFinally() throws JsonProcessingException {
var execution = JacksonMapper.ofJson().readValue("""
{
"tenantId": "main",
"id": "5gokrBx2Bgdwq428pp8FfO",
"namespace": "io.kestra.tests",
"flowId": "restart-with-finally",
"flowRevision": 1,
"taskRunList": [
{
"tenantId": "main",
"id": "6fDC4yvllUoL87vcU20gBW",
"executionId": "5gokrBx2Bgdwq428pp8FfO",
"namespace": "io.kestra.tests",
"flowId": "restart-with-finally",
"taskId": "hello",
"attempts": [
{
"state": {
"current": "SUCCESS",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.269109Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.269111Z"
},
{
"state": "SUCCESS",
"date": "2025-09-23T13:58:11.272912Z"
}
],
"duration": 0.003803000,
"endDate": "2025-09-23T13:58:11.272912Z",
"startDate": "2025-09-23T13:58:11.269109Z"
},
"workerId": "5695d2af-49be-43e7-b31c-64e190150e6b"
}
],
"outputs": {},
"state": {
"current": "SUCCESS",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.244352Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.267718Z"
},
{
"state": "SUCCESS",
"date": "2025-09-23T13:58:11.272936Z"
}
],
"duration": 0.028584000,
"endDate": "2025-09-23T13:58:11.272936Z",
"startDate": "2025-09-23T13:58:11.244352Z"
}
},
{
"tenantId": "main",
"id": "1pPRjIfVKXvOZoLOkABn87",
"executionId": "5gokrBx2Bgdwq428pp8FfO",
"namespace": "io.kestra.tests",
"flowId": "restart-with-finally",
"taskId": "fail_randomly",
"attempts": [
{
"state": {
"current": "FAILED",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.309287Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.309288Z"
},
{
"state": "FAILED",
"date": "2025-09-23T13:58:11.315404Z"
}
],
"duration": 0.006117000,
"endDate": "2025-09-23T13:58:11.315404Z",
"startDate": "2025-09-23T13:58:11.309287Z"
},
"workerId": "5695d2af-49be-43e7-b31c-64e190150e6b"
}
],
"outputs": {},
"state": {
"current": "SKIPPED",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.297189Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.309209Z"
},
{
"state": "FAILED",
"date": "2025-09-23T13:58:11.315426Z"
},
{
"state": "RESTARTED",
"date": "2025-09-23T13:58:11.487704Z"
},
{
"state": "SKIPPED",
"date": "2025-09-23T13:58:11.527186Z"
}
],
"duration": 0.229997000,
"endDate": "2025-09-23T13:58:11.527186Z",
"startDate": "2025-09-23T13:58:11.297189Z"
}
},
{
"tenantId": "main",
"id": "4oOj1UnTnWBOvBHznzBkpS",
"executionId": "5gokrBx2Bgdwq428pp8FfO",
"namespace": "io.kestra.tests",
"flowId": "restart-with-finally",
"taskId": "log",
"attempts": [
{
"state": {
"current": "SUCCESS",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.593248Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.593249Z"
},
{
"state": "SUCCESS",
"date": "2025-09-23T13:58:11.601594Z"
}
],
"duration": 0.008346000,
"endDate": "2025-09-23T13:58:11.601594Z",
"startDate": "2025-09-23T13:58:11.593248Z"
},
"workerId": "5695d2af-49be-43e7-b31c-64e190150e6b"
}
],
"outputs": {},
"state": {
"current": "SUCCESS",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.545959Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.593028Z"
},
{
"state": "SUCCESS",
"date": "2025-09-23T13:58:11.601619Z"
}
],
"duration": 0.055660000,
"endDate": "2025-09-23T13:58:11.601619Z",
"startDate": "2025-09-23T13:58:11.545959Z"
}
}
],
"labels": [
{
"key": "system.correlationId",
"value": "5gokrBx2Bgdwq428pp8FfO"
},
{
"key": "system.restarted",
"value": "true"
}
],
"state": {
"current": "RUNNING",
"histories": [
{
"state": "CREATED",
"date": "2025-09-23T13:58:11.229434Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.244594Z"
},
{
"state": "FAILED",
"date": "2025-09-23T13:58:11.458146Z"
},
{
"state": "RESTARTED",
"date": "2025-09-23T13:58:11.488893Z"
},
{
"state": "RUNNING",
"date": "2025-09-23T13:58:11.526898Z"
}
],
"duration": 0.297464000,
"startDate": "2025-09-23T13:58:11.229434Z"
},
"originalId": "5gokrBx2Bgdwq428pp8FfO",
"deleted": false,
"metadata": {
"attemptNumber": 2,
"originalCreatedDate": "2025-09-23T13:58:11.229439Z"
}
}
""", Execution.class);
executionRepository.save(execution);
jdbcExecutor.executionQueue(Either.left(execution));
Await.until(() -> {
var res = executionRepository.findById("main", "5gokrBx2Bgdwq428pp8FfO");
return res.isPresent() && res.get().getState().isSuccess();
});
}
}

View File

@@ -13,7 +13,7 @@ dependencies {
// versions for libraries with multiple module but no BOM
def slf4jVersion = "2.0.17"
def protobufVersion = "3.25.5" // Orc still uses 3.25.5 see https://github.com/apache/orc/blob/main/java/pom.xml
def bouncycastleVersion = "1.82"
def bouncycastleVersion = "1.81"
def mavenResolverVersion = "2.0.10"
def jollydayVersion = "1.5.6"
def jsonschemaVersion = "4.38.0"
@@ -35,7 +35,7 @@ dependencies {
// we define cloud bom here for GCP, Azure and AWS so they are aligned for all plugins that use them (secret, storage, oss and ee plugins)
api platform('com.google.cloud:libraries-bom:26.68.0')
api platform("com.azure:azure-sdk-bom:1.2.38")
api platform('software.amazon.awssdk:bom:2.34.2')
api platform('software.amazon.awssdk:bom:2.33.11')
api platform("dev.langchain4j:langchain4j-bom:$langchain4jVersion")
api platform("dev.langchain4j:langchain4j-community-bom:$langchain4jCommunityVersion")
@@ -65,8 +65,8 @@ dependencies {
// http5 client
api("org.apache.httpcomponents.client5:httpclient5:5.5")
api("org.apache.httpcomponents.core5:httpcore5:5.3.6")
api("org.apache.httpcomponents.core5:httpcore5-h2:5.3.6")
api("org.apache.httpcomponents.core5:httpcore5:5.3.5")
api("org.apache.httpcomponents.core5:httpcore5-h2:5.3.5")
api("com.fasterxml.uuid:java-uuid-generator:$jugVersion")
// issue with the Docker lib having a too old version for the k8s extension
@@ -75,17 +75,17 @@ dependencies {
api "org.apache.kafka:kafka-clients:$kafkaVersion"
api "org.apache.kafka:kafka-streams:$kafkaVersion"
// AWS CRT is not included in the AWS BOM but needed for the S3 Transfer manager
api 'software.amazon.awssdk.crt:aws-crt:0.39.0'
api 'software.amazon.awssdk.crt:aws-crt:0.38.13'
// we need at least 0.14, it could be removed when Micronaut contains a recent only version in their BOM
api "io.micrometer:micrometer-core:1.15.4"
// We need at least 6.17, it could be removed when Micronaut contains a recent only version in their BOM
api "io.micronaut.openapi:micronaut-openapi-bom:6.18.1"
api "io.micronaut.openapi:micronaut-openapi-bom:6.18.0"
// Other libs
api("org.projectlombok:lombok:1.18.42")
api("org.projectlombok:lombok:1.18.40")
api("org.codehaus.janino:janino:3.1.12")
api group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.25.2'
api group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.25.1'
api group: 'org.slf4j', name: 'jul-to-slf4j', version: slf4jVersion
api group: 'org.slf4j', name: 'jcl-over-slf4j', version: slf4jVersion
api group: 'org.fusesource.jansi', name: 'jansi', version: '2.4.2'
@@ -101,7 +101,7 @@ dependencies {
api group: 'org.apache.maven.resolver', name: 'maven-resolver-connector-basic', version: mavenResolverVersion
api group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-file', version: mavenResolverVersion
api group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-apache', version: mavenResolverVersion
api 'com.github.oshi:oshi-core:6.9.0'
api 'com.github.oshi:oshi-core:6.8.3'
api 'io.pebbletemplates:pebble:3.2.4'
api group: 'co.elastic.logging', name: 'logback-ecs-encoder', version: '1.7.0'
api group: 'de.focus-shift', name: 'jollyday-core', version: jollydayVersion
@@ -124,9 +124,9 @@ dependencies {
api 'org.jsoup:jsoup:1.21.2'
api "org.xhtmlrenderer:flying-saucer-core:$flyingSaucerVersion"
api "org.xhtmlrenderer:flying-saucer-pdf:$flyingSaucerVersion"
api group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.5'
api group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.4'
api group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '3.0.0'
api group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.5'
api group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.4'
api group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '3.2.2'
api group: 'de.siegmar', name: 'fastcsv', version: '4.0.0'
// Json Diff

View File

@@ -14,7 +14,6 @@ import io.kestra.core.models.conditions.ConditionContext;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.ExecutionKilled;
import io.kestra.core.models.executions.ExecutionKilledTrigger;
import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.flows.FlowId;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.FlowWithException;
@@ -38,7 +37,6 @@ import io.micronaut.inject.qualifiers.Qualifiers;
import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -72,7 +70,6 @@ public abstract class AbstractScheduler implements Scheduler {
private final QueueInterface<WorkerJob> workerJobQueue;
private final QueueInterface<WorkerTriggerResult> workerTriggerResultQueue;
private final QueueInterface<ExecutionKilled> executionKilledQueue;
private final QueueInterface<LogEntry> logQueue;
@SuppressWarnings("rawtypes")
private final Optional<QueueInterface> clusterEventQueue;
protected final FlowListenersInterface flowListeners;
@@ -127,7 +124,6 @@ public abstract class AbstractScheduler implements Scheduler {
this.executionKilledQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.KILL_NAMED));
this.workerTriggerResultQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.WORKERTRIGGERRESULT_NAMED));
this.clusterEventQueue = applicationContext.findBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.CLUSTER_EVENT_NAMED));
this.logQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.WORKERTASKLOG_NAMED));
this.flowListeners = flowListeners;
this.runContextFactory = applicationContext.getBean(RunContextFactory.class);
this.runContextInitializer = applicationContext.getBean(RunContextInitializer.class);
@@ -771,7 +767,7 @@ public abstract class AbstractScheduler implements Scheduler {
this.executionEventPublisher.publishEvent(new CrudEvent<>(newExecution, CrudEventType.CREATE));
} catch (QueueException e) {
try {
Execution failedExecution = fail(newExecution, e);
Execution failedExecution = newExecution.failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED);
this.executionQueue.emit(failedExecution);
this.executionEventPublisher.publishEvent(new CrudEvent<>(failedExecution, CrudEventType.CREATE));
} catch (QueueException ex) {
@@ -780,16 +776,6 @@ public abstract class AbstractScheduler implements Scheduler {
}
}
private Execution fail(Execution message, Exception e) {
var failedExecution = message.failedExecutionFromExecutor(e);
try {
logQueue.emitAsync(failedExecution.getLogs());
} catch (QueueException ex) {
// fail silently
}
return failedExecution.getExecution().getState().isFailed() ? failedExecution.getExecution() : failedExecution.getExecution().withState(State.Type.FAILED);
}
private void executionMonitor() {
try {
// Retrieve triggers with non-null execution_id from all corresponding virtual nodes

View File

@@ -12,5 +12,5 @@ dependencies {
api 'org.hamcrest:hamcrest:3.0'
api 'org.hamcrest:hamcrest-library:3.0'
api 'org.mockito:mockito-junit-jupiter'
api 'org.assertj:assertj-core:3.27.6'
api 'org.assertj:assertj-core:3.27.4'
}

View File

@@ -504,7 +504,7 @@
import YAML_CHART from "../dashboard/assets/executions_timeseries_chart.yaml?raw";
import Utils from "../../utils/utils";
import {filterValidLabels} from "./utils"
import {filterLabels} from "./utils"
import {useExecutionsStore} from "../../stores/executions";
import {useAuthStore} from "override/stores/auth";
import {useFlowStore} from "../../stores/flow";
@@ -1055,9 +1055,9 @@
);
},
setLabels() {
const filtered = filterValidLabels(this.executionLabels)
const filtered = filterLabels(this.executionLabels)
if (filtered.error) {
if(filtered.error) {
this.$toast().error(this.$t("wrong labels"))
return;
}

View File

@@ -55,7 +55,7 @@
import LabelInput from "../../components/labels/LabelInput.vue";
import {State} from "@kestra-io/ui-libs"
import {filterValidLabels} from "./utils"
import {filterLabels} from "./utils"
import permission from "../../models/permission";
import action from "../../models/action";
import {useAuthStore} from "override/stores/auth"
@@ -78,11 +78,10 @@
},
methods: {
setLabels() {
const filtered = filterValidLabels(this.executionLabels)
let filtered = filterLabels(this.executionLabels)
if (filtered.error) {
this.$toast().error(this.$t("wrong labels"))
return;
if(filtered.error) {
filtered.labels = filtered.labels.filter(obj => !(obj.key === null && obj.value === null));
}
this.isOpen = false;

View File

@@ -8,7 +8,7 @@ interface FilterResult {
error?: boolean;
}
export const filterValidLabels = (labels: Label[]): FilterResult => {
const validLabels = labels.filter(label => label.key !== null && label.value !== null && label.key !== "" && label.value !== "");
return validLabels.length === labels.length ? {labels} : {labels: validLabels, error: true};
export const filterLabels = (labels: Label[]): FilterResult => {
const invalid = labels.some(label => label.key === null || label.value === null || label.key === "" || label.value === "");
return invalid ? {labels, error: true} : {labels};
};

View File

@@ -351,15 +351,13 @@
const dataTableRef = useTemplateRef<typeof DataTable>("dataTable");
const {queryWithFilter, onPageChanged, onRowDoubleClick, onSort} = useDataTableActions({dblClickRouteName: "flows/update"});
function selectionMapper({id, namespace, disabled}: {id: string; namespace: string; disabled: boolean}) {
function selectionMapper(element: {id: string; namespace: string; disabled: boolean}): {id: string; namespace: string; enabled: boolean} {
return {
id,
namespace,
enabled: !disabled,
id: element.id,
namespace: element.namespace,
enabled: !element.disabled,
};
}
const {selection, queryBulkAction, handleSelectionChange, toggleAllUnselected, toggleAllSelection} = useSelectTableActions({
dataTableRef,
selectionMapper

View File

@@ -242,7 +242,7 @@
return this.kvs?.filter(kv =>
!this.searchQuery ||
kv.key.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
kv.description?.toLowerCase().includes(this.searchQuery.toLowerCase())
kv.description.toLowerCase().includes(this.searchQuery.toLowerCase())
);
},
kvModalTitle() {

View File

@@ -11,9 +11,10 @@
hideToggle
>
<template #header>
<SidebarToggleButton
@toggle="collapsed = onToggleCollapse(!collapsed)"
/>
<el-button @click="collapsed = onToggleCollapse(!collapsed)" class="collapseButton" :size="collapsed ? 'small':undefined">
<ChevronRight v-if="collapsed" />
<ChevronLeft v-else />
</el-button>
<div class="logo">
<component :is="props.showLink ? 'router-link' : 'div'" :to="{name: 'home'}">
<span class="img" />
@@ -40,14 +41,14 @@
import {SidebarMenu} from "vue-sidebar-menu";
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";
import StarOutline from "vue-material-design-icons/StarOutline.vue";
import Environment from "./Environment.vue";
import BookmarkLinkList from "./BookmarkLinkList.vue";
import {useBookmarksStore} from "../../stores/bookmarks";
import type {MenuItem} from "override/components/useLeftMenu";
import {useLayoutStore} from "../../stores/layout";
import SidebarToggleButton from "./SidebarToggleButton.vue";
const props = withDefaults(defineProps<{
@@ -62,11 +63,9 @@
const $route = useRoute()
const {t} = useI18n({useScope: "global"});
const layoutStore = useLayoutStore();
function onToggleCollapse(folded) {
collapsed.value = folded;
layoutStore.setSideMenuCollapsed(folded);
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
$emit("menu-collapse", folded);
return folded;

View File

@@ -1,43 +0,0 @@
<template>
<el-button
class="collapseButton sidebar-toggle"
@click="$emit('toggle')"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.8554 10.9932C11.8567 11.4542 11.4841 11.8289 11.0231 11.8301L1.02524 11.858C0.564312 11.8593 0.189613 11.4867 0.188327 11.0258L0.160404 1.01728C0.159118 0.556349 0.531732 0.181649 0.99266 0.180363L10.9906 0.152469C11.4515 0.151183 11.8262 0.523797 11.8275 0.984726L11.8554 10.9932ZM11.0318 11.0054L5.18316 11.0217L5.15511 0.967535L11.0037 0.951218L11.0318 11.0054ZM4.31027 11.023L0.975876 11.0323L0.947825 0.978203L4.28221 0.9689L4.31027 11.023Z"
fill="currentColor"
/>
</svg>
</el-button>
</template>
<script setup lang="ts">
defineEmits<{
(e: "toggle"): void;
}>();
</script>
<style lang="scss" scoped>
.sidebar-toggle {
border: none;
color: var(--ks-text-secondary);
&:hover {
color: var(--ks-content-link);
}
html.dark & {
color: var(--ks-text-secondary);
}
}
</style>

View File

@@ -1,40 +1,32 @@
<template>
<nav data-component="FILENAME_PLACEHOLDER" class="d-flex w-100 gap-3 top-bar">
<div class="d-flex flex-column flex-grow-1 flex-shrink-1 overflow-hidden top-title">
<div class="d-flex align-items-end gap-2">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
<el-breadcrumb v-if="breadcrumb">
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">
<a v-if="item.disabled || !item.link">
{{ item.label }}
</a>
<router-link v-else :to="item.link">
{{ item.label }}
</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
<h1 class="h5 fw-semibold m-0 d-inline-flex">
<slot name="title">
{{ title }}
<el-tooltip v-if="description" :content="description">
<Information class="ms-2" />
</el-tooltip>
<Badge v-if="beta" label="Beta" />
</slot>
<el-button
class="star-button"
:class="{'star-active': bookmarked}"
:icon="bookmarked ? StarIcon : StarOutlineIcon"
circle
@click="onStarClick"
/>
<div class="d-flex flex-column gap-2">
<el-breadcrumb v-if="breadcrumb">
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">
<a v-if="item.disabled || !item.link">
{{ item.label }}
</a>
<router-link v-else :to="item.link">
{{ item.label }}
</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
<h1 class="h5 fw-semibold m-0 d-inline-flex">
<slot name="title">
{{ title }}
<el-tooltip v-if="description" :content="description">
<Information class="ms-2" />
</el-tooltip>
<Badge v-if="beta" label="Beta" />
</slot>
<el-button
class="star-button"
:class="{'star-active': bookmarked}"
:icon="bookmarked ? StarIcon : StarOutlineIcon"
circle
@click="onStarClick"
/>
</h1>
</div>
</div>
</h1>
</div>
<div class="d-lg-flex side gap-2 flex-shrink-0 align-items-center mycontainer">
<div class="d-none d-lg-flex align-items-center">
@@ -69,8 +61,6 @@
import {useBookmarksStore} from "../../stores/bookmarks";
import {useToast} from "../../utils/toast";
import {useFlowStore} from "../../stores/flow";
import {useLayoutStore} from "../../stores/layout";
import SidebarToggleButton from "./SidebarToggleButton.vue";
const props = defineProps<{
title: string;
@@ -83,7 +73,6 @@
const bookmarksStore = useBookmarksStore();
const flowStore = useFlowStore();
const route = useRoute();
const layoutStore = useLayoutStore();
const shouldDisplayDeleteButton = computed(() => {

View File

@@ -24,7 +24,7 @@
const route = useRoute();
const context = computed(() => ({title:details.value.title}));
const context = computed(() => details.value.title);
useRouteContext(context);
const namespace = computed(() => route.params?.id) as Ref<string>;

View File

@@ -40,50 +40,58 @@
</div>
</template>
<script setup lang="ts">
import {computed, onMounted} from "vue";
<script setup>
import Markdown from "../layout/Markdown.vue";
import {SchemaToHtml, TaskIcon} from "@kestra-io/ui-libs";
import GitHub from "vue-material-design-icons/Github.vue";
</script>
<script>
import intro from "../../assets/docs/basic.md?raw";
import {getPluginReleaseUrl} from "../../utils/pluginUtils";
import {mapStores} from "pinia";
import {usePluginsStore} from "../../stores/plugins";
import {useMiscStore} from "override/stores/misc";
const props = withDefaults(defineProps<{
overrideIntro?: string | null;
absolute?: boolean;
fetchPluginDocumentation?: boolean;
}>(), {
overrideIntro: null,
absolute: false,
fetchPluginDocumentation: true
});
const pluginsStore = usePluginsStore();
const miscStore = useMiscStore();
const introContent = computed(() => props.overrideIntro ?? intro);
const pluginName = computed(() => {
if (!pluginsStore.editorPlugin?.cls) return "";
const split = pluginsStore.editorPlugin.cls.split(".");
return split[split.length - 1];
});
const releaseNotesUrl = computed(() =>
pluginsStore.editorPlugin?.cls ? getPluginReleaseUrl(pluginsStore.editorPlugin.cls) : null
);
function openReleaseNotes() {
if (releaseNotesUrl.value) {
window.open(releaseNotesUrl.value, "_blank");
export default {
props: {
overrideIntro: {
type: String,
default: null
},
absolute: {
type: Boolean,
default: false
},
fetchPluginDocumentation: {
type: Boolean,
default: true
}
},
computed: {
...mapStores(usePluginsStore, useMiscStore),
introContent () {
return this.overrideIntro ?? intro
},
pluginName() {
const split = this.pluginsStore.editorPlugin.cls.split(".");
return split[split.length - 1];
},
releaseNotesUrl() {
return getPluginReleaseUrl(this.pluginsStore.editorPlugin.cls);
}
},
created() {
this.pluginsStore.list();
},
methods: {
openReleaseNotes() {
if (this.releaseNotesUrl) {
window.open(this.releaseNotesUrl, "_blank");
}
}
}
}
onMounted(() => {
pluginsStore.list();
});
</script>
<style scoped lang="scss">

View File

@@ -36,7 +36,7 @@ function statsGlobalData(config: Config, uid: string): any {
export async function initPostHogForSetup(config: Config): Promise<void> {
try {
if (!config.isUiAnonymousUsageEnabled || import.meta.env.MODE === "development") return
if (!config.isUiAnonymousUsageEnabled) return
const apiStore = useApiStore()
const apiConfig = await apiStore.loadConfig()

View File

@@ -1,18 +1,10 @@
import {ref, computed, Ref} from "vue"
import {ref, computed} from "vue"
export function useSelectTableActions({
dataTableRef,
selectionMapper
}: {
dataTableRef: Ref<any>
selectionMapper?: (element: any) => any
}) {
export function useSelectTableActions(selectTableRef: any) {
const queryBulkAction = ref(false)
const selection = ref<any[]>([])
const elTable = computed(() => dataTableRef.value?.$refs?.table)
selectionMapper = selectionMapper ?? ((element: any) => element)
const elTable = computed(() => selectTableRef.value?.$refs?.table)
const handleSelectionChange = (value: any[]) => {
selection.value = value.map(selectionMapper)
@@ -30,6 +22,8 @@ export function useSelectTableActions({
queryBulkAction.value = true
}
const selectionMapper = (element: any) => element
return {
queryBulkAction,
selection,

View File

@@ -1,5 +1,5 @@
<template>
<LeftMenu v-if="miscStore.configs && !layoutStore.sideMenuCollapsed" @menu-collapse="onMenuCollapse" />
<LeftMenu v-if="miscStore.configs" @menu-collapse="onMenuCollapse" />
<main>
<Errors v-if="coreStore.error" :code="coreStore.error" />
<slot v-else />
@@ -17,20 +17,20 @@
import Errors from "../../../components/errors/Errors.vue"
import ContextInfoBar from "../../../components/ContextInfoBar.vue"
import SurveyDialog from "../../../components/SurveyDialog.vue"
import {onMounted, ref, watch} from "vue"
import {onMounted, ref} from "vue"
import {useSurveySkip} from "../../../composables/useSurveyData"
import {useCoreStore} from "../../../stores/core"
import {useMiscStore} from "override/stores/misc"
import {useLayoutStore} from "../../../stores/layout"
const coreStore = useCoreStore()
const miscStore = useMiscStore()
const layoutStore = useLayoutStore()
const {markSurveyDialogShown} = useSurveySkip()
const showSurveyDialog = ref(false)
const onMenuCollapse = (collapse) => {
layoutStore.setSideMenuCollapsed(collapse)
const htmlElement = document.documentElement
htmlElement.classList.toggle("menu-collapsed", collapse)
htmlElement.classList.toggle("menu-not-collapsed", !collapse)
}
const handleSurveyDialogClose = () => {
@@ -49,11 +49,8 @@
}
onMounted(() => {
onMenuCollapse(layoutStore.sideMenuCollapsed)
const isMenuCollapsed = localStorage.getItem("menuCollapsed") === "true"
onMenuCollapse(isMenuCollapsed)
checkForSurveyDialog()
})
watch(() => layoutStore.sideMenuCollapsed, (val) => {
onMenuCollapse(val)
})
</script>

View File

@@ -4,15 +4,13 @@ interface State {
topNavbar: any | undefined;
envName: string | undefined;
envColor: string | undefined;
sideMenuCollapsed: boolean;
}
export const useLayoutStore = defineStore("layout", {
state: (): State => ({
topNavbar: undefined,
envName: localStorage.getItem("envName") || undefined,
envColor: localStorage.getItem("envColor") || undefined,
sideMenuCollapsed: localStorage.getItem("menuCollapsed") === "true",
envColor: localStorage.getItem("envColor") || undefined
}),
getters: {},
actions: {
@@ -36,15 +34,6 @@ export const useLayoutStore = defineStore("layout", {
localStorage.removeItem("envColor");
}
this.envColor = value;
},
setSideMenuCollapsed(value: boolean) {
this.sideMenuCollapsed = value;
localStorage.setItem("menuCollapsed", value ? "true" : "false");
const htmlElement = document.documentElement;
htmlElement.classList.toggle("menu-collapsed", value);
htmlElement.classList.toggle("menu-not-collapsed", !value);
},
},
}
}
});

View File

@@ -52,7 +52,7 @@ interface State {
type?: string;
version?: string;
};
forceIncludeProperties?: string[];
forceIncludeProperties?: Record<string, any>;
_iconsPromise: Promise<Record<string, string>> | undefined;
}