Compare commits

...

40 Commits

Author SHA1 Message Date
YannC.
aafc4c326b fix: kv test remove content type 2025-11-04 08:03:00 +01:00
YannC.
461058c13e chore: add multipart vendor annotations for custom generation on SDK 2025-11-03 14:59:06 +01:00
YannC.
b7512c3124 fix: KV command test 2025-11-03 08:50:18 +01:00
YannC.
2bd51ccdec fix: only use plain-text for setKeyValue endpoint 2025-11-03 08:16:27 +01:00
nKwiatkowski
ee9193c4d5 feat(API): add multipart to openAPI 2025-11-03 08:16:27 +01:00
nKwiatkowski
d3e5293ab7 feat(API): add multipart to openAPI 2025-11-03 08:16:27 +01:00
Roman Acevedo
68336c753d Revert "add back , deprecated = false on flow update, otherwise its marked as deprecated"
This reverts commit 3772404b68f14f0a80af9e0adb9952d58e9102b4.
2025-11-03 08:16:27 +01:00
Roman Acevedo
73f3471c0e add back , deprecated = false on flow update, otherwise its marked as deprecated 2025-11-03 08:16:27 +01:00
Roman Acevedo
4012f74e43 change KV schema type to be object 2025-11-03 08:16:27 +01:00
YannC.
d79a0d3fb2 fix: inputs/outputs as object 2025-11-03 08:16:27 +01:00
YannC.
5720682d2c fix: optional params in delete executions endpoints 2025-11-03 08:16:27 +01:00
YannC.
d9c5b274d3 fix(flowController): set correct hidden for json method in 2025-11-03 08:16:27 +01:00
Roman Acevedo
a816dff4b0 feat: add typing indication to validateTask 2025-11-03 08:16:26 +01:00
YannC.
0d31e140b5 feat: executions annotations for skipping, follow method generation in sdk 2025-11-03 08:16:26 +01:00
nKwiatkowski
e61d5568df clean(API): add deprecated on open api 2025-11-03 08:16:26 +01:00
Roman Acevedo
e7216d9f6b fix: flow update not deprecated 2025-11-03 08:16:26 +01:00
nKwiatkowski
adfe389c7b clean(API): add query to filter parameter 2025-11-03 08:16:26 +01:00
YannC.
47ab4ce9d1 fix: kv controller remove namespace check 2025-11-03 08:16:26 +01:00
Roman Acevedo
d10893ca00 ci: switch to new release docker plugin list and add dry run 2025-10-31 20:13:22 +01:00
Loïc Mathieu
c5ef356a1c fix(executions): Flow triggered twice when there are two multiple conditions
Fixes #12560
2025-10-31 16:26:22 +01:00
Dnyanesh Pise
0313e8e49b fix(ui): prevent marking fields as error on login (Fix #12548) (#12554) 2025-10-30 23:38:16 +05:30
Loïc Mathieu
f4b6161f14 fix(executions): set the execution to KILLING and not RESTARTED when killing a paused flow
Fixes https://github.com/kestra-io/kestra/issues/12417
2025-10-30 18:13:57 +01:00
Bart Ledoux
e69e82a35e fix: make switch statements work 2025-10-30 16:07:08 +01:00
Loïc Mathieu
e77378bcb7 chore(deps): fix OpenTelemetry proto so it works with Protobuf 3
Fixes https://github.com/kestra-io/kestra/issues/12298
2025-10-30 15:49:09 +01:00
Hemant M Mehta
3c9df90a35 fix(executions): jq-filter-zip-exception
closes: #11683
2025-10-30 12:57:53 +01:00
YannC
6c86f0917c fix: make sure taskOutputs is never set as a Variables map (#12484)
close #11967
2025-10-29 15:26:14 +01:00
Your Name
30b7346ee0 fix(core): handle integer size in chunk Pebble filter 2025-10-29 12:37:31 +01:00
Naveen Gowda MY
2f485c74ff fix(core): add error feedback and validation (#12472) 2025-10-29 15:53:50 +05:30
brian-mulier-p
3a5713bbd1 fix(core): show tasks in JSON Schema for Switch.cases (#12478)
part of #10508
2025-10-29 11:01:17 +01:00
Roman Acevedo
2eed738b83 ci: add skip test param to pre-release.yml 2025-10-28 17:54:26 +01:00
brian.mulier
5e2609ce5e chore(version): update to version '1.0.8' 2025-10-28 14:37:22 +01:00
Florian Hussonnois
86f909ce93 fix(flows): KV pebble expressions with input defaults (#12314)
Fixes: #12314
2025-10-28 14:32:44 +01:00
Loïc Mathieu
a8cb28a127 fix(executions): remove errors and finally tasks when restarting
Otherwize we would detect that an error or a finally branch is processing and the flowable state would not be correctly taken.

Moreover, it prevent this branch to be taken again after a restart.

Fixes #11731
2025-10-28 14:30:27 +01:00
brian.mulier
0fe9ba3e13 fix(tests): was missing some utils 2025-10-28 12:31:59 +01:00
brian-mulier-p
40f5aadd1a fix(kv): don't throw in KV function with errorOnMissing=false for expired kv (#12321)
closes #12294
2025-10-24 11:42:02 +02:00
Bart Ledoux
ceac25429a fix(ui): update ui-libs to make docs work
closes #12252
2025-10-23 12:24:13 +02:00
Bart Ledoux
4144d9fbb1 build: avoid using posthog in development 2025-10-23 12:21:41 +02:00
Florian Hussonnois
9cc7d45f74 fix(core): allow secrets to be render for multiselect (#12045)
Fix: #12045
2025-10-23 11:32:21 +02:00
Florian Hussonnois
81ee330b9e fix(core): ignore not found plugin types for schema generation 2025-10-23 11:32:10 +02:00
Hemant M Mehta
5382655a2e fix: file-download-issue (#11774)
* fix: file-download-issue

closes: #11569

* fix: test case

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>

---------

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>
2025-10-22 11:49:54 +02:00
47 changed files with 1105 additions and 430 deletions

View File

@@ -5,6 +5,15 @@ on:
tags:
- 'v*'
workflow_dispatch:
inputs:
skip-test:
description: 'Skip test'
type: choice
required: true
default: 'false'
options:
- "true"
- "false"
jobs:
build-artifacts:
@@ -14,6 +23,7 @@ jobs:
backend-tests:
name: Backend tests
uses: kestra-io/actions/.github/workflows/kestra-oss-backend-tests.yml@main
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
secrets:
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -23,6 +33,7 @@ jobs:
frontend-tests:
name: Frontend tests
uses: kestra-io/actions/.github/workflows/kestra-oss-frontend-tests.yml@main
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
secrets:
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -13,11 +13,11 @@ on:
required: true
type: boolean
default: false
plugin-version:
description: 'Plugin version'
required: false
type: string
default: "LATEST"
dry-run:
description: 'Dry run mode that will not write or release anything'
required: true
type: boolean
default: false
jobs:
publish-docker:
@@ -25,9 +25,9 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v')
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-docker.yml@main
with:
plugin-version: ${{ inputs.plugin-version }}
retag-latest: ${{ inputs.retag-latest }}
retag-lts: ${{ inputs.retag-lts }}
dry-run: ${{ inputs.dry-run }}
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}

View File

@@ -62,7 +62,7 @@ public class KvUpdateCommand extends AbstractApiCommand {
Duration ttl = expiration == null ? null : Duration.parse(expiration);
MutableHttpRequest<String> request = HttpRequest
.PUT(apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/kv/" + key, value)
.contentType(MediaType.APPLICATION_JSON_TYPE);
.contentType(MediaType.TEXT_PLAIN);
if (ttl != null) {
request.header("ttl", ttl.toString());

View File

@@ -15,6 +15,7 @@ import com.github.victools.jsonschema.generator.impl.DefinitionKey;
import com.github.victools.jsonschema.generator.naming.DefaultSchemaDefinitionNamingStrategy;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
import com.github.victools.jsonschema.module.jackson.JsonUnwrappedDefinitionProvider;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
@@ -45,6 +46,9 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.*;
import java.time.*;
@@ -58,7 +62,9 @@ import static io.kestra.core.docs.AbstractClassDocumentation.required;
import static io.kestra.core.serializers.JacksonMapper.MAP_TYPE_REFERENCE;
@Singleton
@Slf4j
public class JsonSchemaGenerator {
private static final List<Class<?>> TYPES_RESOLVED_AS_STRING = List.of(Duration.class, LocalTime.class, LocalDate.class, LocalDateTime.class, ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class);
private static final List<Class<?>> SUBTYPE_RESOLUTION_EXCLUSION_FOR_PLUGIN_SCHEMA = List.of(Task.class, AbstractTrigger.class);
@@ -270,8 +276,22 @@ public class JsonSchemaGenerator {
.with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
.with(Option.DEFINITION_FOR_MAIN_SCHEMA)
.with(Option.PLAIN_DEFINITION_KEYS)
.with(Option.ALLOF_CLEANUP_AT_THE_END);;
.with(Option.ALLOF_CLEANUP_AT_THE_END);
// HACK: Registered a custom JsonUnwrappedDefinitionProvider prior to the JacksonModule
// to be able to return an CustomDefinition with an empty node when the ResolvedType can't be found.
builder.forTypesInGeneral().withCustomDefinitionProvider(new JsonUnwrappedDefinitionProvider(){
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
try {
return super.provideCustomSchemaDefinition(javaType, context);
} catch (NoClassDefFoundError e) {
// This error happens when a non-supported plugin type exists in the classpath.
log.debug("Cannot create schema definition for type '{}'. Cause: NoClassDefFoundError", javaType.getTypeName());
return new CustomDefinition(context.getGeneratorConfig().createObjectNode(), true);
}
}
});
if (!draft7) {
builder.with(new JacksonModule(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM));
} else {
@@ -300,6 +320,7 @@ public class JsonSchemaGenerator {
// inline some type
builder.forTypesInGeneral()
.withCustomDefinitionProvider(new CustomDefinitionProviderV2() {
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
if (javaType.isInstanceOf(Map.class) || javaType.isInstanceOf(Enum.class)) {

View File

@@ -28,6 +28,7 @@ import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.MapUtils;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@@ -77,10 +78,12 @@ public class Execution implements DeletedInterface, TenantInterface {
@With
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Schema(implementation = Object.class)
Map<String, Object> inputs;
@With
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Schema(implementation = Object.class)
Map<String, Object> outputs;
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
@@ -88,6 +91,7 @@ public class Execution implements DeletedInterface, TenantInterface {
List<Label> labels;
@With
@Schema(implementation = Object.class)
Map<String, Object> variables;
@NotNull
@@ -936,7 +940,15 @@ public class Execution implements DeletedInterface, TenantInterface {
for (TaskRun current : taskRuns) {
if (!MapUtils.isEmpty(current.getOutputs())) {
if (current.getIteration() != null) {
taskOutputs = MapUtils.merge(taskOutputs, outputs(current, byIds));
Map<String, Object> merged = MapUtils.merge(taskOutputs, outputs(current, byIds));
// If one of two of the map is null in the merge() method, we just return the other
// And if the not null map is a Variables (= read only), we cast it back to a simple
// hashmap to avoid taskOutputs becoming read-only
// i.e this happen in nested loopUntil tasks
if (merged instanceof Variables) {
merged = new HashMap<>(merged);
}
taskOutputs = merged;
} else {
taskOutputs.putAll(outputs(current, byIds));
}

View File

@@ -7,6 +7,7 @@ import io.kestra.core.models.tasks.ResolvedTask;
import io.kestra.core.models.tasks.retrys.AbstractRetry;
import io.kestra.core.utils.IdUtils;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@@ -53,6 +54,7 @@ public class TaskRun implements TenantInterface {
@With
@JsonInclude(JsonInclude.Include.ALWAYS)
@Nullable
@Schema(implementation = Object.class)
Variables outputs;
@NotNull

View File

@@ -282,9 +282,10 @@ public class FlowInputOutput {
Input<?> input = resolvable.get().input();
try {
// resolve all input dependencies and check whether input is enabled
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, flow, execution, inputs, decryptSecrets);
final RunContext runContext = buildRunContextForExecutionAndInputs(flow, execution, dependencies, decryptSecrets);
// Resolve all input dependencies and check whether input is enabled
// Note: Secrets are always decrypted here because they can be part of expressions used to render inputs such as SELECT & MULTI_SELECT.
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, flow, execution, inputs, true);
final RunContext runContext = buildRunContextForExecutionAndInputs(flow, execution, dependencies, true);
boolean isInputEnabled = dependencies.isEmpty() || dependencies.values().stream().allMatch(InputAndValue::enabled);
@@ -324,7 +325,8 @@ public class FlowInputOutput {
// resolve default if needed
if (value == null && input.getDefaults() != null) {
value = resolveDefaultValue(input, runContext);
RunContext runContextForDefault = decryptSecrets ? runContext : buildRunContextForExecutionAndInputs(flow, execution, dependencies, false);
value = resolveDefaultValue(input, runContextForDefault);
resolvable.isDefault(true);
}

View File

@@ -10,7 +10,6 @@ import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.input.SecretInput;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.property.PropertyContext;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
@@ -100,7 +99,7 @@ public final class RunVariables {
* @return a new immutable {@link Map}.
*/
static Map<String, Object> of(final AbstractTrigger trigger) {
return ImmutableMap.of(
return Map.of(
"id", trigger.getId(),
"type", trigger.getType()
);
@@ -282,12 +281,15 @@ public final class RunVariables {
}
if (flow != null && flow.getInputs() != null) {
// Create a new PropertyContext with 'flow' variables which are required by some pebble expressions.
PropertyContextWithVariables context = new PropertyContextWithVariables(propertyContext, Map.of("flow", RunVariables.of(flow)));
// we add default inputs value from the flow if not already set, this will be useful for triggers
flow.getInputs().stream()
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
.forEach(input -> {
try {
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, propertyContext));
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, context));
} catch (IllegalVariableEvaluationException e) {
// Silent catch, if an input depends on another input, or a variable that is populated at runtime / input filling time, we can't resolve it here.
}
@@ -391,4 +393,20 @@ public final class RunVariables {
}
private RunVariables(){}
private record PropertyContextWithVariables(
PropertyContext delegate,
Map<String, Object> variables
) implements PropertyContext {
@Override
public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return delegate.render(inline, variables.isEmpty() ? this.variables : variables);
}
@Override
public Map<String, Object> render(Map<String, Object> inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return delegate.render(inline, variables.isEmpty() ? this.variables : variables);
}
}
}

View File

@@ -1,14 +1,15 @@
package io.kestra.core.runners.pebble.filters;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.Filter;
import io.pebbletemplates.pebble.template.EvaluationContext;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import java.util.List;
import java.util.Map;
public class ChunkFilter implements Filter {
@Override
public List<String> getArgumentNames() {
@@ -30,6 +31,10 @@ public class ChunkFilter implements Filter {
throw new PebbleException(null, "'chunk' filter can only be applied to List. Actual type was: " + input.getClass().getName(), lineNumber, self.getName());
}
return Lists.partition((List) input, ((Long) args.get("size")).intValue());
Object sizeObj = args.get("size");
if (!(sizeObj instanceof Number)) {
throw new PebbleException(null, "'chunk' filter argument 'size' must be a number. Actual type was: " + sizeObj.getClass().getName(), lineNumber, self.getName());
}
return Lists.partition((List) input, ((Number) sizeObj).intValue());
}
}

View File

@@ -17,12 +17,17 @@ import java.util.List;
import java.util.Map;
public class JqFilter implements Filter {
private final Scope scope;
// Load Scope once as static to avoid repeated initialization
// This improves performance by loading builtin functions only once when the class loads
private static final Scope SCOPE;
private final List<String> argumentNames = new ArrayList<>();
static {
SCOPE = Scope.newEmptyScope();
BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, SCOPE);
}
public JqFilter() {
scope = Scope.newEmptyScope();
BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, scope);
this.argumentNames.add("expression");
}
@@ -43,10 +48,7 @@ public class JqFilter implements Filter {
String pattern = (String) args.get("expression");
Scope rootScope = Scope.newEmptyScope();
BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, rootScope);
try {
JsonQuery q = JsonQuery.compile(pattern, Versions.JQ_1_6);
JsonNode in;
@@ -59,7 +61,7 @@ public class JqFilter implements Filter {
final List<Object> out = new ArrayList<>();
try {
q.apply(scope, in, v -> {
q.apply(Scope.newChildScope(SCOPE), in, v -> {
if (v instanceof TextNode) {
out.add(v.textValue());
} else if (v instanceof NullNode) {

View File

@@ -38,7 +38,7 @@ public class KvFunction implements Function {
String key = getKey(args, self, lineNumber);
String namespace = (String) args.get(NAMESPACE_ARG);
Boolean errorOnMissing = Optional.ofNullable((Boolean) args.get(ERROR_ON_MISSING_ARG)).orElse(true);
boolean errorOnMissing = Optional.ofNullable((Boolean) args.get(ERROR_ON_MISSING_ARG)).orElse(true);
Map<String, String> flow = (Map<String, String>) context.getVariable("flow");
String flowNamespace = flow.get(NAMESPACE_ARG);
@@ -53,11 +53,16 @@ public class KvFunction implements Function {
// we didn't check allowedNamespace here as it's checked in the kvStoreService itself
value = kvStoreService.get(flowTenantId, namespace, flowNamespace).getValue(key);
}
} catch (ResourceExpiredException e) {
if (errorOnMissing) {
throw new PebbleException(e, e.getMessage(), lineNumber, self.getName());
}
value = Optional.empty();
} catch (Exception e) {
throw new PebbleException(e, e.getMessage(), lineNumber, self.getName());
}
if (value.isEmpty() && errorOnMissing == Boolean.TRUE) {
if (value.isEmpty() && errorOnMissing) {
throw new PebbleException(null, "The key '" + key + "' does not exist in the namespace '" + namespace + "'.", lineNumber, self.getName());
}
@@ -85,4 +90,4 @@ public class KvFunction implements Function {
return (String) args.get(KEY_ARGS);
}
}
}

View File

@@ -14,6 +14,7 @@ import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.input.InputAndValue;
import io.kestra.core.models.hierarchies.AbstractGraphTask;
import io.kestra.core.models.hierarchies.GraphCluster;
import io.kestra.core.models.tasks.FlowableTask;
import io.kestra.core.models.tasks.ResolvedTask;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.tasks.retrys.AbstractRetry;
@@ -121,21 +122,38 @@ public class ExecutionService {
* Retry set the given taskRun in created state
* and return the execution in running state
**/
public Execution retryTask(Execution execution, String taskRunId) {
List<TaskRun> newTaskRuns = execution
.getTaskRunList()
.stream()
.map(taskRun -> {
if (taskRun.getId().equals(taskRunId)) {
return taskRun
.withState(State.Type.CREATED);
public Execution retryTask(Execution execution, Flow flow, String taskRunId) throws InternalException {
TaskRun taskRun = execution.findTaskRunByTaskRunId(taskRunId).withState(State.Type.CREATED);
List<TaskRun> taskRunList = execution.getTaskRunList();
if (taskRun.getParentTaskRunId() != null) {
// we need to find the parent to remove any errors or finally tasks already executed
TaskRun parentTaskRun = execution.findTaskRunByTaskRunId(taskRun.getParentTaskRunId());
Task parentTask = flow.findTaskByTaskId(parentTaskRun.getTaskId());
if (parentTask instanceof FlowableTask<?> flowableTask) {
if (flowableTask.getErrors() != null) {
List<Task> allErrors = Stream.concat(flowableTask.getErrors().stream()
.filter(task -> task.isFlowable() && ((FlowableTask<?>) task).getErrors() != null)
.flatMap(task -> ((FlowableTask<?>) task).getErrors().stream()),
flowableTask.getErrors().stream())
.toList();
allErrors.forEach(error -> taskRunList.removeIf(t -> t.getTaskId().equals(error.getId())));
}
return taskRun;
})
.toList();
if (flowableTask.getFinally() != null) {
List<Task> allFinally = Stream.concat(flowableTask.getFinally().stream()
.filter(task -> task.isFlowable() && ((FlowableTask<?>) task).getFinally() != null)
.flatMap(task -> ((FlowableTask<?>) task).getFinally().stream()),
flowableTask.getFinally().stream())
.toList();
allFinally.forEach(error -> taskRunList.removeIf(t -> t.getTaskId().equals(error.getId())));
}
}
return execution.withTaskRunList(newTaskRuns).withState(State.Type.RUNNING);
return execution.withTaskRunList(taskRunList).withTaskRun(taskRun).withState(State.Type.RUNNING);
}
return execution.withTaskRun(taskRun).withState(State.Type.RUNNING);
}
public Execution retryWaitFor(Execution execution, String flowableTaskRunId) {
@@ -709,7 +727,7 @@ public class ExecutionService {
// An edge case can exist where the execution is resumed automatically before we resume it with a killing.
try {
newExecution = this.resume(execution, flow, State.Type.KILLING, null);
newExecution = newExecution.withState(afterKillState.orElse(newExecution.getState().getCurrent()));
newExecution = newExecution.withState(killingOrAfterKillState);
} catch (Exception e) {
// if we cannot resume, we set it anyway to killing, so we don't throw
log.warn("Unable to resume a paused execution before killing it", e);
@@ -723,6 +741,7 @@ public class ExecutionService {
// immediately without publishing a CrudEvent like it's done on pause/resume method.
return newExecution;
}
public Execution kill(Execution execution, FlowInterface flow) {
return this.kill(execution, flow, Optional.empty());
}

View File

@@ -102,7 +102,7 @@ public class Switch extends Task implements FlowableTask<Switch.Output> {
@Schema(
title = "The map of keys and a list of tasks to be executed if the conditional `value` matches the key"
)
@PluginProperty
@PluginProperty(additionalProperties = Task[].class)
private Map<String, List<Task>> cases;
@Valid

View File

@@ -173,8 +173,8 @@ public class Download extends AbstractHttp implements RunnableTask<Download.Outp
if (path.indexOf('/') != -1) {
path = path.substring(path.lastIndexOf('/')); // keep the last segment
}
if (path.indexOf('.') != -1) {
return path.substring(path.indexOf('.'));
if (path.lastIndexOf('.') != -1) {
return path.substring(path.lastIndexOf('.'));
}
return null;
}

View File

@@ -119,6 +119,7 @@ class ExecutionServiceTest {
assertThat(restart.getState().getHistories()).hasSize(4);
assertThat(restart.getTaskRunList().stream().filter(taskRun -> taskRun.getState().getCurrent() == State.Type.RESTARTED).count()).isGreaterThan(1L);
assertThat(restart.getTaskRunList().stream().filter(taskRun -> taskRun.getState().getCurrent() == State.Type.RUNNING).count()).isGreaterThan(1L);
assertThat(restart.getTaskRunList().getFirst().getId()).isEqualTo(restart.getTaskRunList().getFirst().getId());
assertThat(restart.getLabels()).contains(new Label(Label.RESTARTED, "true"));
}
@@ -413,9 +414,9 @@ class ExecutionServiceTest {
Execution killed = executionService.kill(execution, flow);
assertThat(killed.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
assertThat(killed.getState().getCurrent()).isEqualTo(State.Type.KILLING);
assertThat(killed.findTaskRunsByTaskId("pause").getFirst().getState().getCurrent()).isEqualTo(State.Type.KILLED);
assertThat(killed.getState().getHistories()).hasSize(4);
assertThat(killed.getState().getHistories()).hasSize(5);
}
@Test

View File

@@ -8,17 +8,23 @@ import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.FileInput;
import io.kestra.core.models.flows.input.InputAndValue;
import io.kestra.core.models.flows.input.IntInput;
import io.kestra.core.models.flows.input.MultiselectInput;
import io.kestra.core.models.flows.input.StringInput;
import io.kestra.core.models.property.Property;
import io.kestra.core.secret.SecretNotFoundException;
import io.kestra.core.secret.SecretService;
import io.kestra.core.services.KVStoreService;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.storages.kv.InternalKVStore;
import io.kestra.core.storages.kv.KVStore;
import io.kestra.core.storages.kv.KVValue;
import io.kestra.core.utils.IdUtils;
import io.micronaut.http.MediaType;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.http.multipart.CompletedPart;
import io.micronaut.test.annotation.MockBean;
import jakarta.inject.Inject;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
@@ -34,11 +40,13 @@ import java.util.Map;
import java.util.Optional;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@KestraTest
class FlowInputOutputTest {
private static final String TEST_SECRET_VALUE = "test-secret-value";
private static final String TEST_KV_VALUE = "test-kv-value";
static final Execution DEFAULT_TEST_EXECUTION = Execution.builder()
.id(IdUtils.create())
@@ -63,6 +71,21 @@ class FlowInputOutputTest {
};
}
@MockBean(KVStoreService.class)
KVStoreService testKVStoreService() {
return new KVStoreService() {
@Override
public KVStore get(String tenant, String namespace, @Nullable String fromNamespace) {
return new InternalKVStore(tenant, namespace, storageInterface) {
@Override
public Optional<KVValue> getValue(String key) {
return Optional.of(new KVValue(TEST_KV_VALUE));
}
};
}
};
}
@Test
void shouldResolveEnabledInputsGivenInputWithConditionalExpressionMatchingTrue() {
// Given
@@ -318,6 +341,24 @@ class FlowInputOutputTest {
Assertions.assertEquals("******", results.getFirst().value());
}
@Test
void shouldNotObfuscateSecretsInSelectWhenValidatingInputs() {
// Given
MultiselectInput input = MultiselectInput.builder()
.id("input")
.type(Type.MULTISELECT)
.expression("{{ [secret('???')] }}")
.required(false)
.build();
// When
List<InputAndValue> results = flowInputOutput.validateExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, Mono.empty()).block();
// Then
Assertions.assertEquals(TEST_SECRET_VALUE, ((MultiselectInput)results.getFirst().input()).getValues().getFirst());
}
@Test
void shouldNotObfuscateSecretsWhenReadingInputs() {
// Given
@@ -335,6 +376,23 @@ class FlowInputOutputTest {
Assertions.assertEquals(TEST_SECRET_VALUE, results.get("input"));
}
@Test
void shouldEvaluateExpressionOnDefaultsUsingKVFunction() {
// Given
StringInput input = StringInput.builder()
.id("input")
.type(Type.STRING)
.defaults(Property.ofExpression("{{ kv('???') }}"))
.required(false)
.build();
// When
Map<String, Object> results = flowInputOutput.readExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, Mono.empty()).block();
// Then
assertThat(results.get("input")).isEqualTo(TEST_KV_VALUE);
}
private static class MemoryCompletedPart implements CompletedPart {
protected final String name;

View File

@@ -1,9 +1,11 @@
package io.kestra.core.runners;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.DependsOn;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.GenericFlow;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.BoolInput;
import io.kestra.core.models.property.Property;
@@ -11,25 +13,55 @@ import io.kestra.core.models.property.PropertyContext;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.runners.pebble.PebbleEngineFactory;
import io.kestra.core.services.KVStoreService;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.storages.kv.InternalKVStore;
import io.kestra.core.storages.kv.KVStore;
import io.kestra.core.storages.kv.KVValue;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.IdUtils;
import io.micronaut.context.ApplicationContext;
import io.micronaut.test.annotation.MockBean;
import jakarta.inject.Inject;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest
class RunVariablesTest {
private final PropertyContext propertyContext = Mockito.mock(PropertyContext.class);
@Inject
VariableRenderer renderer;
@Inject
StorageInterface storageInterface;
@MockBean(KVStoreService.class)
KVStoreService testKVStoreService() {
return new KVStoreService() {
@Override
public KVStore get(String tenant, String namespace, @Nullable String fromNamespace) {
return new InternalKVStore(tenant, namespace, storageInterface) {
@Override
public Optional<KVValue> getValue(String key) {
return Optional.of(new KVValue("value"));
}
};
}
};
}
@Test
@SuppressWarnings("unchecked")
void shouldGetEmptyVariables() {
Map<String, Object> variables = new RunVariables.DefaultBuilder().build(new RunContextLogger(), propertyContext);
Map<String, Object> variables = new RunVariables.DefaultBuilder().build(new RunContextLogger(), PropertyContext.create(renderer));
assertThat(variables.size()).isEqualTo(3);
assertThat((Map<String, Object>) variables.get("envs")).isEqualTo(Map.of());
assertThat((Map<String, Object>) variables.get("globals")).isEqualTo(Map.of());
@@ -46,7 +78,7 @@ class RunVariablesTest {
.revision(42)
.build()
)
.build(new RunContextLogger(), propertyContext);
.build(new RunContextLogger(), PropertyContext.create(renderer));
Assertions.assertEquals(Map.of(
"id", "id-value",
"namespace", "namespace-value",
@@ -65,7 +97,7 @@ class RunVariablesTest {
.tenantId("tenant-value")
.build()
)
.build(new RunContextLogger(), propertyContext);
.build(new RunContextLogger(), PropertyContext.create(renderer));
Assertions.assertEquals(Map.of(
"id", "id-value",
"namespace", "namespace-value",
@@ -88,7 +120,7 @@ class RunVariablesTest {
return "type-value";
}
})
.build(new RunContextLogger(), propertyContext);
.build(new RunContextLogger(), PropertyContext.create(renderer));
Assertions.assertEquals(Map.of("id", "id-value", "type", "type-value"), variables.get("task"));
}
@@ -106,7 +138,7 @@ class RunVariablesTest {
return "type-value";
}
})
.build(new RunContextLogger(), propertyContext);
.build(new RunContextLogger(), PropertyContext.create(renderer));
Assertions.assertEquals(Map.of("id", "id-value", "type", "type-value"), variables.get("trigger"));
}
@@ -115,7 +147,7 @@ class RunVariablesTest {
void shouldGetKestraConfiguration() {
Map<String, Object> variables = new RunVariables.DefaultBuilder()
.withKestraConfiguration(new RunVariables.KestraConfiguration("test", "http://localhost:8080"))
.build(new RunContextLogger(), propertyContext);
.build(new RunContextLogger(), PropertyContext.create(renderer));
assertThat(variables.size()).isEqualTo(4);
Map<String, Object> kestra = (Map<String, Object>) variables.get("kestra");
assertThat(kestra).hasSize(2);
@@ -124,7 +156,7 @@ class RunVariablesTest {
}
@Test
void nonResolvableDynamicInputsShouldBeSkipped() throws IllegalVariableEvaluationException {
void nonResolvableDynamicInputsShouldBeSkipped() {
VariableRenderer.VariableConfiguration mkVariableConfiguration = Mockito.mock(VariableRenderer.VariableConfiguration.class);
ApplicationContext mkApplicationContext = Mockito.mock(ApplicationContext.class);
Map<String, Object> variables = new RunVariables.DefaultBuilder()
@@ -145,4 +177,23 @@ class RunVariablesTest {
"a", true
), variables.get("inputs"));
}
@Test
void shouldBuildVariablesGivenFlowWithInputHavingDefaultPebbleExpression() {
FlowInterface flow = GenericFlow.fromYaml(TenantService.MAIN_TENANT, """
id: id-value
namespace: namespace-value
inputs:
- id: input
type: STRING
defaults: "{{ kv('???') }}"
""");
Map<String, Object> variables = new RunVariables.DefaultBuilder()
.withFlow(flow)
.withExecution(Execution.builder().id(IdUtils.create()).build())
.build(new RunContextLogger(), PropertyContext.create(renderer));
assertThat(variables.get("inputs")).isEqualTo(Map.of("input", "value"));
}
}

View File

@@ -38,4 +38,17 @@ class ChunkFilterTest {
}).get();
});
}
@Test
void chunkWithIntegerVariable() throws IllegalVariableEvaluationException {
// Reproducer for issue: Integer variable causing ClassCastException
Map<String, Object> vars = Map.of(
"max_items", Integer.valueOf(2),
"list", Arrays.asList(1, 2, 3, 4, 5)
);
String render = variableRenderer.render("{{ list | chunk(max_items) }}", vars);
assertThat(render).isEqualTo("[[1,2],[3,4],[5]]");
}
}

View File

@@ -14,10 +14,14 @@ public class FunctionTestUtils {
}
public static Map<String, Object> getVariables(String namespace) {
return FunctionTestUtils.getVariables(MAIN_TENANT, namespace);
}
public static Map<String, Object> getVariables(String tenant, String namespace) {
return Map.of(
"flow", Map.of(
"id", "kv",
"tenantId", MAIN_TENANT,
"tenantId", tenant,
"namespace", namespace)
);
}

View File

@@ -1,25 +1,30 @@
package io.kestra.core.runners.pebble.functions;
import static io.kestra.core.runners.pebble.functions.FunctionTestUtils.getVariables;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.assertThat;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.runners.VariableRenderer;
import io.kestra.core.storages.StorageContext;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.storages.kv.InternalKVStore;
import io.kestra.core.storages.kv.KVMetadata;
import io.kestra.core.storages.kv.KVStore;
import io.kestra.core.storages.kv.KVValueAndMetadata;
import io.kestra.core.utils.TestsUtils;
import jakarta.inject.Inject;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import static io.kestra.core.runners.pebble.functions.FunctionTestUtils.getVariables;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest(startRunner = true)
public class KvFunctionTest {
@@ -107,6 +112,25 @@ public class KvFunctionTest {
assertThat(rendered).isEqualTo("");
}
@Test
void shouldThrowOrGetEmptyIfExpiredDependingOnErrorOnMissing() throws IOException, IllegalVariableEvaluationException {
String tenant = TestsUtils.randomTenant();
String namespace = TestsUtils.randomNamespace();
Map<String, Object> variables = getVariables(tenant, namespace);
KVStore kv = new InternalKVStore(tenant, namespace, storageInterface);
kv.put("my-expired-key", new KVValueAndMetadata(new KVMetadata(null, Instant.now().minus(1, ChronoUnit.HOURS)), "anyValue"));
String rendered = variableRenderer.render("{{ kv('my-expired-key', errorOnMissing=false) }}", variables);
assertThat(rendered).isEqualTo("");
kv.put("another-expired-key", new KVValueAndMetadata(new KVMetadata(null, Instant.now().minus(1, ChronoUnit.HOURS)), "anyValue"));
IllegalVariableEvaluationException exception = Assertions.assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ kv('another-expired-key') }}", variables));
assertThat(exception.getMessage()).isEqualTo("io.pebbletemplates.pebble.error.PebbleException: The requested value has expired ({{ kv('another-expired-key') }}:1)");
}
@Test
void shouldFailGivenNonExistingKeyAndErrorOnMissingTrue() {
// Given
@@ -126,9 +150,7 @@ public class KvFunctionTest {
// Given
Map<String, Object> variables = getVariables("io.kestra.tests");
// When
IllegalVariableEvaluationException exception = Assertions.assertThrows(IllegalVariableEvaluationException.class, () -> {
variableRenderer.render("{{ kv('my-key') }}", variables);
});
IllegalVariableEvaluationException exception = Assertions.assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ kv('my-key') }}", variables));
// Then
assertThat(exception.getMessage()).isEqualTo("io.pebbletemplates.pebble.error.PebbleException: The key 'my-key' does not exist in the namespace 'io.kestra.tests'. ({{ kv('my-key') }}:1)");

View File

@@ -12,5 +12,7 @@ class FileUtilsTest {
assertThat(FileUtils.getExtension("")).isNull();
assertThat(FileUtils.getExtension("/file/hello")).isNull();
assertThat(FileUtils.getExtension("/file/hello.txt")).isEqualTo(".txt");
assertThat(FileUtils.getExtension("/file/hello.file with spaces.txt")).isEqualTo(".txt");
assertThat(FileUtils.getExtension("/file/hello.file.with.multiple.dots.txt")).isEqualTo(".txt");
}
}

View File

@@ -231,4 +231,9 @@ public class RetryCaseTest {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.FAILED);
}
public void retryWithFlowableErrors(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.getTaskRunList()).hasSize(3);
assertThat(execution.getTaskRunList().get(2).attemptNumber()).isEqualTo(2);
}
}

View File

@@ -217,6 +217,25 @@ class DownloadTest {
assertThat(output.getUri().toString()).endsWith("filename..jpg");
}
@Test
void contentDispositionWithSpaceAfterDot() throws Exception {
EmbeddedServer embeddedServer = applicationContext.getBean(EmbeddedServer.class);
embeddedServer.start();
Download task = Download.builder()
.id(DownloadTest.class.getSimpleName())
.type(DownloadTest.class.getName())
.uri(Property.ofValue(embeddedServer.getURI() + "/content-disposition-space-after-dot"))
.build();
RunContext runContext = TestsUtils.mockRunContext(this.runContextFactory, task, ImmutableMap.of());
Download.Output output = task.run(runContext);
assertThat(output.getUri().toString()).doesNotContain("/secure-path/");
assertThat(output.getUri().toString()).endsWith("file.with+spaces.txt");
}
@Controller()
public static class SlackWebController {
@Get("500")
@@ -257,5 +276,11 @@ class DownloadTest {
return HttpResponse.ok("Hello World".getBytes())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"/secure-path/filename..jpg\"");
}
@Get("content-disposition-space-after-dot")
public HttpResponse<byte[]> contentDispositionWithSpaceAfterDot() {
return HttpResponse.ok("Hello World".getBytes())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"file.with spaces.txt\"");
}
}
}

View File

@@ -0,0 +1,29 @@
id: retry-with-flowable-errors
namespace: io.kestra.tests
tasks:
- id: set_kv
type: io.kestra.plugin.core.kv.Set
key: "retry_counter_1"
value: "1"
kvType: NUMBER
overwrite: true
- id: retry_block
type: io.kestra.plugin.core.flow.AllowFailure
tasks:
- id: run_script
type: io.kestra.plugin.core.log.Log
message: "{{kv(namespace=flow.namespace, key='retry_counter_1') < 2 ? ko : 'It works'}}"
errors:
- id: incr_counter
type: io.kestra.plugin.core.kv.Set
key: retry_counter_1
value: "{{ kv(namespace=flow.namespace, key='retry_counter_1') + 1 }}"
overwrite: true
retry:
type: constant
behavior: RETRY_FAILED_TASK
maxAttempts: 3
interval: PT0.5S
warningOnRetry: true

View File

@@ -1,6 +1,6 @@
version=1.0.7
version=1.0.8
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.priority=low
org.gradle.priority=low

View File

@@ -1204,6 +1204,7 @@ public class JdbcExecutor implements ExecutorInterface {
flowTriggerService.withFlowTriggersOnly(allFlows.stream())
.filter(f -> ListUtils.emptyOnNull(f.getTrigger().getConditions()).stream().anyMatch(c -> c instanceof MultipleCondition) || f.getTrigger().getPreconditions() != null)
.map(f -> new MultipleConditionEvent(f.getFlow(), execution))
.distinct() // we can have multiple MultipleConditionEvent if a flow contains multiple triggers as it would lead to multiple FlowWithFlowTrigger
.forEach(throwConsumer(multipleCondition -> multipleConditionEventQueue.emit(multipleCondition)));
}
@@ -1270,6 +1271,7 @@ public class JdbcExecutor implements ExecutorInterface {
else if (executionDelay.getDelayType().equals(ExecutionDelay.DelayType.RESTART_FAILED_TASK)) {
Execution newAttempt = executionService.retryTask(
pair.getKey(),
findFlow(pair.getKey()),
executionDelay.getTaskRunId()
);
executor = executor.withExecution(newAttempt, "retryFailedTask");

View File

@@ -137,4 +137,10 @@ public abstract class JdbcRunnerRetryTest {
void retryDynamicTask(Execution execution){
retryCaseTest.retryDynamicTask(execution);
}
@Test
@ExecuteFlow("flows/valids/retry-with-flowable-errors.yaml")
void retryWithFlowableErrors(Execution execution){
retryCaseTest.retryWithFlowableErrors(execution);
}
}

View File

@@ -39,6 +39,8 @@ dependencies {
api platform("dev.langchain4j:langchain4j-community-bom:$langchain4jCommunityVersion")
constraints {
// downgrade to proto 1.3.2-alpha as 1.5.0 needs protobuf 4
api("io.opentelemetry.proto:opentelemetry-proto:1.3.2-alpha")
// need to force this dep as mysql-connector brings a version incompatible with the Google Cloud libs
api("com.google.protobuf:protobuf-java:$protobufVersion")
api("com.google.protobuf:protobuf-java-util:$protobufVersion")

View File

@@ -28,6 +28,7 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
@@ -37,6 +38,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
@@ -50,6 +52,45 @@ abstract public class TestsUtils {
queueConsumersCancellations.get().clear();
}
public static String randomNamespace(String... prefix) {
return TestsUtils.randomString(prefix);
}
public static String randomTenant(String... prefix) {
return TestsUtils.randomString(prefix);
}
private static String[] stackTraceToParts() {
// We take the stacktrace from the util caller to troubleshoot more easily
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[4];
String[] packageSplit = stackTraceElement.getClassName().split("\\.");
return new String[]{packageSplit[packageSplit.length - 1].toLowerCase(), stackTraceElement.getMethodName().toLowerCase()};
}
/**
* there is at least one bug in {@link io.kestra.cli.services.FileChangedEventListener#getTenantIdFromPath(Path)} forbidding use to use '_' character
* @param prefix
* @return
*/
private static String randomString(String... prefix) {
if (prefix.length == 0) {
prefix = new String[]{String.join("-", stackTraceToParts())};
}
var tenantRegex = "^[a-z0-9][a-z0-9_-]*";
var validTenantPrefixes = Arrays.stream(prefix)
.map(s -> s.replace(".", "-"))
.map(String::toLowerCase)
.peek(p -> {
if (!p.matches(tenantRegex)) {
throw new IllegalArgumentException("random tenant prefix %s should match tenant regex %s".formatted(p, tenantRegex));
}
}).toList();
String[] parts = Stream
.concat(validTenantPrefixes.stream(), Stream.of(IdUtils.create().toLowerCase()))
.toArray(String[]::new);
return IdUtils.fromPartsAndSeparator('-',parts);
}
public static <T> T map(String path, Class<T> cls) throws IOException {
URL resource = TestsUtils.class.getClassLoader().getResource(path);
assert resource != null;

36
ui/package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.255",
"@kestra-io/ui-libs": "^0.0.260",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.46.2",
@@ -3202,9 +3202,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@kestra-io/ui-libs": {
"version": "0.0.255",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.255.tgz",
"integrity": "sha512-HzuAzQJOBSmXqKWbi+b0z3PwdRJphtKLBJ8LEbtBYRaIF4SfA9uDndVquOeTM1pORpLmO8Gw51Us4ZcUnABNZw==",
"version": "0.0.260",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.260.tgz",
"integrity": "sha512-m71BkH1n5PfSdywlg5Gaq9TnFiX67u6SH///JnsQyCTx7u2pLfVav5YDejVNFrPG9qyvJRnEuNSFxmBCMB8L2g==",
"dependencies": {
"@nuxtjs/mdc": "^0.17.3",
"@popperjs/core": "^2.11.8",
@@ -4127,13 +4127,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.55.0"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -17509,13 +17509,13 @@
}
},
"node_modules/playwright": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.0"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -17528,9 +17528,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -20783,9 +20783,9 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.255",
"@kestra-io/ui-libs": "^0.0.260",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.46.2",

View File

@@ -4,9 +4,9 @@
<Logo class="logo" />
</div>
<el-form @submit.prevent :model="credentials" ref="form">
<el-form @submit.prevent :model="credentials" ref="form" :rules="rules" :show-message="false">
<input type="hidden" name="from" :value="redirectPath">
<el-form-item>
<el-form-item prop="username">
<el-input
name="username"
size="large"
@@ -14,14 +14,18 @@
v-model="credentials.username"
:placeholder="t('email')"
required
prop="username"
>
<template #prepend>
<Account />
</template>
<template #suffix v-if="getFieldError('username')">
<el-tooltip placement="top" :content="getFieldError('username')">
<InformationOutline class="validation-icon error" />
</el-tooltip>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-form-item prop="password">
<el-input
v-model="credentials.password"
size="large"
@@ -31,11 +35,15 @@
type="password"
show-password
required
prop="password"
>
<template #prepend>
<Lock />
</template>
<template #suffix v-if="getFieldError('password')">
<el-tooltip placement="top" :content="getFieldError('password')">
<InformationOutline class="validation-icon error" />
</el-tooltip>
</template>
</el-input>
</el-form-item>
<el-form-item>
@@ -44,7 +52,7 @@
class="w-100"
size="large"
native-type="submit"
@click="handleSubmit"
@click.prevent="handleSubmit"
:disabled="isLoginDisabled"
:loading="isLoading"
>
@@ -73,9 +81,11 @@
import {ElMessage} from "element-plus"
import type {FormInstance} from "element-plus"
import axios from "axios"
import MailChecker from "mailchecker"
import Account from "vue-material-design-icons/Account.vue"
import Lock from "vue-material-design-icons/Lock.vue"
import InformationOutline from "vue-material-design-icons/InformationOutline.vue"
import Logo from "../home/Logo.vue"
import {useCoreStore} from "../../stores/core"
@@ -104,11 +114,47 @@
password: ""
})
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*\d)\S{8,}$/
const validateEmail = (_rule: any, value: string, callback: (error?: Error) => void) => {
if (!value?.trim()) {
return callback(new Error(t("setup.validation.email_required")));
} else if (!EMAIL_REGEX.test(value)) {
return callback(new Error(t("setup.validation.email_invalid")));
} else if (!MailChecker.isValid(value)) {
return callback(new Error(t("setup.validation.email_temporary_not_allowed")));
} else {
callback();
}
};
const validatePassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
if (!value || !PASSWORD_REGEX.test(value)) {
return callback(new Error(t("setup.validation.password_invalid")));
}
callback();
};
const rules = computed(() => ({
username: [{required: true, validator: validateEmail, trigger: "blur"}],
password: [{required: true, validator: validatePassword, trigger: "blur"}]
}))
const getFieldError = (fieldName: string) => {
if (!form.value) return null
const field = form.value.fields?.find((f: any) => f.prop === fieldName)
return field?.validateState === "error" ? field.validateMessage : null
}
const redirectPath = computed(() => (route.query.from as string) ?? "/welcome")
const isLoginDisabled = computed(() =>
!credentials.value.username?.trim() ||
!credentials.value.password?.trim() ||
!EMAIL_REGEX.test(credentials.value.username) ||
!PASSWORD_REGEX.test(credentials.value.password) ||
!MailChecker.isValid(credentials.value.username) ||
isLoading.value
)
@@ -136,13 +182,13 @@
const handleNetworkError = (error: any) => {
return error.code === "ERR_NETWORK" ||
error.code === "ECONNREFUSED" ||
(!error.response && error.message.includes("Network Error"))
(!error.response && error.message?.includes("Network Error"))
}
const loadAuthConfigErrors = async (showIncorrectCredsMessage = true) => {
const loadAuthConfigErrors = async () => {
try {
const errors = await miscStore.loadBasicAuthValidationErrors()
if (errors && errors.length > 0) {
if (errors?.length) {
errors.forEach((error: string) => {
ElMessage.error({
message: `${error}. ${t("setup.validation.config_message")}`,
@@ -150,24 +196,23 @@
showClose: false
})
})
} else if (showIncorrectCredsMessage) {
ElMessage.error(t("setup.validation.incorrect_creds"))
}
} catch (error) {
console.error("Failed to load auth config errors:", error)
} catch {
ElMessage.error({
message: t("setup.validation.incorrect_creds")
})
}
}
const handleSubmit = async (event: Event) => {
coreStore.error = undefined;
event.preventDefault()
if (!form.value || isLoading.value) return
if (!(await form.value.validate().catch(() => false))) return
isLoading.value = true
const handleSubmit = async () => {
try {
coreStore.error = undefined;
if (!form.value || isLoading.value) return
if (!(await form.value.validate().catch(() => false))) return
isLoading.value = true
const {username, password} = credentials.value
if (!username?.trim() || !password?.trim()) {
@@ -203,7 +248,7 @@
}
if (error?.response?.status === 401) {
await loadAuthConfigErrors(true)
await loadAuthConfigErrors()
} else if (error?.response?.status === 404) {
router.push({name: "setup"})
} else {
@@ -255,6 +300,13 @@
}
}
}
.validation-icon {
font-size: 1.25em;
&.error {
color: var(--ks-content-alert);
}
}
}
}
</style>

View File

@@ -7,37 +7,67 @@
:closable="false"
class="mb-2"
/>
<el-row v-for="(item, index) in currentValue" :key="index" :gutter="10" class="w-100" :data-testid="`task-dict-item-${item[0]}-${index}`">
<el-col :span="6">
<InputText
:model-value="item[0]"
@update:model-value="onKey(index, $event)"
margin="m-0"
placeholder="Key"
:have-error="duplicatedKeys.includes(item[0])"
/>
</el-col>
<el-col :span="16">
<component
:is="schema.additionalProperties ? getTaskComponent(schema.additionalProperties) : TaskExpression"
:model-value="item[1]"
@update:model-value="onValueChange(index, $event)"
:root="getKey(item[0])"
:schema="schema.additionalProperties"
:required="isRequired(item[0])"
:definitions="definitions"
:disabled
/>
</el-col>
<el-col :span="2" class="col align-self-center delete">
<DeleteOutline @click="removeItem(index)" />
</el-col>
</el-row>
<Add v-if="!disabledAdding" @add="addItem()" />
<template v-if="componentType">
<Wrapper v-for="(item, index) in currentValue" :key="index" class="item-wrapper">
<template #tasks>
<InputText
:model-value="item[0]"
@update:model-value="onKey(index, $event)"
margin="m-0"
placeholder="Key"
:have-error="duplicatedKeys.includes(item[0])"
/>
<hr>
<component
ref="valueComponent"
:is="componentType"
:model-value="item[1]"
@update:model-value="onValueChange(index, $event)"
:root="getKey(item[0])"
:schema="schema.additionalProperties"
:required="isRequired(item[0])"
:disabled
merge
/>
<div class="delete-container">
<button @click="removeItem(index)" class="remove-entry">
{{ te(`no_code.remove.${root}`) ? t(`no_code.remove.${root}`) : t('no_code.remove.default') }} <DeleteOutline />
</button>
</div>
</template>
</Wrapper>
</template>
<template v-else>
<el-row v-for="(item, index) in currentValue" :key="index" :gutter="10" class="w-100" :data-testid="`task-dict-item-${item[0]}-${index}`">
<el-col :span="6">
<InputText
:model-value="item[0]"
@update:model-value="onKey(index, $event)"
margin="m-0"
placeholder="Key"
:have-error="duplicatedKeys.includes(item[0])"
/>
</el-col>
<el-col :span="16">
<TaskExpression
:model-value="item[1]"
@update:model-value="onValueChange(index, $event)"
:root="getKey(item[0])"
:schema="schema.additionalProperties"
:required="isRequired(item[0])"
:disabled
/>
</el-col>
<el-col :span="2" class="col align-self-center delete">
<DeleteOutline @click="removeItem(index)" />
</el-col>
</el-row>
</template>
<Add v-if="!props.disabled" :disabled="addButtonDisabled" @add="addItem()" />
</template>
<script lang="ts" setup>
import {computed, ref, watch} from "vue";
<script setup lang="ts">
import {computed, ref, useTemplateRef, watch} from "vue";
import {useI18n} from "vue-i18n";
import {DeleteOutline} from "../../utils/icons";
@@ -46,35 +76,32 @@
import Add from "../Add.vue";
import getTaskComponent from "./getTaskComponent";
import debounce from "lodash/debounce";
import Wrapper from "./Wrapper.vue";
const {t} = useI18n();
const {t, te} = useI18n();
defineOptions({
name: "TaskDict",
inheritAttrs: false,
});
const props = defineProps({
modelValue: {
type: Object,
default: () => ({}),
},
schema: {
type: Object,
required: true,
},
definitions: {
type: Object,
default: () => ({}),
},
root: {
type: String,
default: undefined,
},
disabled: {
type: Boolean,
default: false,
},
const valueComponent = useTemplateRef<any[]>("valueComponent");
const props = withDefaults(defineProps<{
modelValue?: Record<string, any>;
schema?: any;
root?: string;
disabled?: boolean;
definitions?: Record<string, any>;
}>(), {
disabled: false,
modelValue: () => ({}),
root: undefined,
schema: () => ({type: "object"}),
definitions: () => ({}),
});
const componentType = computed(() => {
return props.schema.additionalProperties ? getTaskComponent(props.schema.additionalProperties, props.root, props.definitions) : null;
});
const currentValue = ref<[string, any][]>([])
@@ -143,15 +170,50 @@
}
function addItem() {
if(addButtonDisabled.value) {
return;
}
currentValue.value.push(["", undefined]);
emitUpdate()
}
const disabledAdding = computed(() => {
return props.disabled || currentValue.value.at(-1)?.[0] === "" && currentValue.value.at(-1)?.[1] === undefined;
const addButtonDisabled = computed(() => {
return currentValue.value.at(-1)?.[0] === "" && currentValue.value.at(-1)?.[1] === undefined;
});
</script>
<style scoped lang="scss">
@import "../../styles/code.scss";
</style>
.task-container{
margin-bottom: 1rem;
}
.delete-container{
display: flex;
align-items: center;
margin-left: 1rem;
justify-content: end;
}
.remove-entry{
color: var(--ks-content-secondary);
background-color: var(--ks-button-background-secondary);
border: none;
display: flex;
align-items: center;
gap: .5rem;
opacity: 0.7;
padding: 0;
height: .75rem;
&:hover {
color: var(--ks-content-secondary);
opacity: 1;
}
}
.item-wrapper {
margin: .25rem 0;
background-color: var(--ks-background-card);
}
</style>

View File

@@ -1,23 +1,81 @@
<template>
<div class="tasks-wrapper">
<Collapse
:title="root"
:elements="items"
:section
:block-schema-path="[blockSchemaPath, 'properties', root, 'items'].join('/')"
@remove="removeItem"
@reorder="(yaml) => flowStore.flowYaml = yaml"
/>
<el-collapse v-model="expanded" class="collapse">
<el-collapse-item
:name="section"
:title="`${section}${elements ? ` (${elements.length})` : ''}`"
:disabled="merge"
:class="{merge}"
>
<template #icon>
<Creation
:parent-path-complete
:ref-path="elements?.length ? elements.length - 1 : undefined"
:block-schema-path
/>
</template>
<Element
v-for="(element, elementIndex) in filteredElements"
:key="elementIndex"
:section
:parent-path-complete
:element
:element-index
:moved="elementIndex == movedIndex"
:block-schema-path
:type-field-schema
@remove-element="removeElement(elementIndex)"
@move-element="
(direction: 'up' | 'down') =>
moveElement(
elements,
element.id,
elementIndex,
direction,
)
"
/>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup lang="ts">
import {computed, inject, ref} from "vue";
import Collapse from "../collapse/Collapse.vue";
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
import {useFlowStore} from "../../../../stores/flow";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import {CollapseItem} from "../../utils/types";
import {
CREATING_TASK_INJECTION_KEY, FULL_SCHEMA_INJECTION_KEY, FULL_SOURCE_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY,
} from "../../injectionKeys";
import {SECTIONS_MAP} from "../../../../utils/constants";
import {getValueAtJsonPath} from "../../../../utils/utils";
import {useI18n} from "vue-i18n";
import Creation from "../collapse/buttons/Creation.vue";
import Element from "../collapse/Element.vue";
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""))
const blockSchemaPathInjected = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""))
const schemaAtBlockPathInjected = computed(() => getValueAtJsonPath(fullSchema.value, blockSchemaPathInjected.value))
const blockSchemaPath = computed(() => {
const rootParts = props.root ? props.root.split(".") : []
if(rootParts.length > 1){
// if second part is a property not defined in properties,
// it can only be defined by additionalProperties
const s = schemaAtBlockPathInjected.value?.properties?.[rootParts[0]]
if(s && s.properties?.[rootParts[1]] === undefined && s.additionalProperties){
rootParts[1] = "additionalProperties"
} else {
rootParts.splice(1, 0, "properties")
}
}
return [blockSchemaPathInjected.value, "properties", ...rootParts, "items"].join("/");
});
defineOptions({
inheritAttrs: false
@@ -34,36 +92,111 @@
const props = withDefaults(defineProps<{
modelValue?: Task[],
root?: string;
merge?: boolean;
}>(), {
modelValue: () => [],
root: undefined
root: undefined,
merge: false,
});
const items = computed(() =>
const elements = computed(() =>
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
);
function removeItem(yaml: string, index: number){
flowStore.flowYaml = yaml;
if(items.value.length <= 1 && index === 0){
function removeElement(index: number){
if(elements.value.length <= 1){
emits("update:modelValue", undefined);
return;
return
}
let localItems = [...items.value]
let localItems = [...elements.value]
localItems.splice(index, 1)
emits("update:modelValue", localItems);
};
const {t} = useI18n();
const section = computed(() => {
return props.root ?? "tasks";
if(props.merge){
return t("tasks");
}
return props.root ?? t("tasks");
});
const flow = inject(FULL_SOURCE_INJECTION_KEY, ref(""));
const filteredElements = computed(() => elements.value?.filter(Boolean) ?? []);
const expanded = props.merge ? computed(() => section.value) : ref<CollapseItem["title"]>(props.root ?? "tasks");
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
const creatingTask = inject(CREATING_TASK_INJECTION_KEY, false);
const parentPathComplete = computed(() => {
return `${[
[
parentPath,
creatingTask && refPath !== undefined
? `[${refPath + 1}]`
: refPath !== undefined
? `[${refPath}]`
: undefined,
].filter(Boolean).join(""),
section.value
].filter(p => p.length).join(".")}`;
});
const movedIndex = ref(-1);
const moveElement = (
items: Record<string, any>[] | undefined,
elementID: string,
index: number,
direction: "up" | "down",
) => {
const keyName = section.value === "Plugin Defaults" ? "type" : "id";
if (!items || !flow) return;
if (
(direction === "up" && index === 0) ||
(direction === "down" && index === items.length - 1)
)
return;
const newIndex = direction === "up" ? index - 1 : index + 1;
movedIndex.value = newIndex;
setTimeout(() => {
movedIndex.value = -1;
}, 200);
flowStore.flowYaml =
YAML_UTILS.swapBlocks({
source:flow.value,
section: SECTIONS_MAP[section.value.toLowerCase() as keyof typeof SECTIONS_MAP],
key1:elementID,
key2:items[newIndex][keyName],
keyName,
})
};
const fullSchema = inject(FULL_SCHEMA_INJECTION_KEY, ref<Record<string, any>>({}));
const blockSchema = computed(() => getValueAtJsonPath(fullSchema.value, blockSchemaPath.value) ?? {});
// resolve parentPathComplete field schema from pluginsStore
const typeFieldSchema = computed(() => blockSchema.value?.type ? "type" : blockSchema.value?.on ? "on" : "type");
</script>
<style scoped lang="scss">
@import "../../styles/code.scss";
.list-header{
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 1rem;
}
.tasks-wrapper {
width: 100%;
}
@@ -73,4 +206,8 @@
pointer-events: none;
cursor: not-allowed;
}
</style>
.merge :deep(.el-collapse-item__header){
cursor: default;
}
</style>

View File

@@ -1,4 +1,5 @@
import {pascalCase} from "change-case";
import {resolve$ref} from "../../../../utils/utils";
const TasksComponents = import.meta.glob<{ default: any }>("./Task*.vue", {eager: true});
@@ -70,7 +71,8 @@ function getType(property: any, key?: string, schema?: any): string {
}
if (property.type === "array") {
if (property.items?.anyOf?.length === 0 || property.items?.anyOf?.length > 10 || key === "pluginDefaults" || key === "layout") {
const items = schema ? resolve$ref({definitions: schema}, property.items) : property.items;
if (items?.anyOf?.length === 0 || items?.anyOf?.length > 10 || key === "pluginDefaults" || key === "layout") {
return "list";
}

View File

@@ -475,7 +475,7 @@
return this.namespacesStore
.createKv({
...this.kv,
contentType: ["DATE", "DATETIME"].includes(type) ? "text/plain" : "application/json",
contentType: "text/plain",
value
})
.then(() => {

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) return
if (!config.isUiAnonymousUsageEnabled || import.meta.env.MODE === "development") return
const apiStore = useApiStore()
const apiConfig = await apiStore.loadConfig()

View File

@@ -1081,7 +1081,7 @@
"cron": "Cron",
"execution_failed": "Execution failed! Last error was",
"execution restarted": "This execution has been restarted {nbRestart} time(s).",
"execution replay": "This execution is a replay of <code>{originalId}</code>.",
"execution replay": "This execution is a replay of <code>{originalId}</code>.",
"execution replayed": "This execution has been replayed.",
"task run id": "TaskRun ID",
"active": "Active",
@@ -1201,6 +1201,7 @@
}
},
"select": {
"default": "Select a type",
"task": "Select a task",
"tasks": "Select a task",
"triggers": "Select a trigger",
@@ -1212,6 +1213,7 @@
"inputs": "Select an input field type"
},
"creation": {
"default": "Add",
"tasks": "Add a task",
"triggers": "Add a trigger",
"errors": "Add an error handler",
@@ -1230,6 +1232,10 @@
"input": "Close input",
"pluginDefaults": "Close plugin default",
"conditions": "Close condition"
},
"remove": {
"default": "Remove this entry",
"cases": "Remove this case"
}
},
"properties": {

View File

@@ -71,6 +71,9 @@ import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -121,7 +124,7 @@ public class ExecutionController {
@Nullable
@Value("${micronaut.server.context-path}")
protected String basePath;
@Inject
private FlowRepositoryInterface flowRepository;
@@ -168,7 +171,7 @@ public class ExecutionController {
@Inject
private ApplicationEventPublisher<CrudEvent<Execution>> eventPublisher;
@Inject
private RunContextFactory runContextFactory;
@@ -186,7 +189,7 @@ public class ExecutionController {
@Inject
private Optional<OpenTelemetry> openTelemetry;
@Inject
private ExecutionStreamingService executionStreamingService;
@@ -206,7 +209,7 @@ public class ExecutionController {
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
//Deprecated params
@Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@@ -355,9 +358,9 @@ public class ExecutionController {
@ApiResponse(responseCode = "204", description = "On success")
public HttpResponse<Void> deleteExecution(
@Parameter(description = "The execution id") @PathVariable String executionId,
@Parameter(description = "Whether to delete execution logs") @QueryValue(defaultValue = "true") Boolean deleteLogs,
@Parameter(description = "Whether to delete execution metrics") @QueryValue(defaultValue = "true") Boolean deleteMetrics,
@Parameter(description = "Whether to delete execution files in the internal storage") @QueryValue(defaultValue = "true") Boolean deleteStorage
@Parameter(description = "Whether to delete execution logs", required = false) @QueryValue(defaultValue = "true") Boolean deleteLogs,
@Parameter(description = "Whether to delete execution metrics", required = false) @QueryValue(defaultValue = "true") Boolean deleteMetrics,
@Parameter(description = "Whether to delete execution files in the internal storage", required = false) @QueryValue(defaultValue = "true") Boolean deleteStorage
) throws IOException {
Optional<Execution> execution = executionRepository.findById(tenantService.resolveTenant(), executionId);
if (execution.isPresent()) {
@@ -376,9 +379,9 @@ public class ExecutionController {
public MutableHttpResponse<?> deleteExecutionsByIds(
@RequestBody(description = "The execution id") @Body List<String> executionsId,
@Parameter(description = "Whether to delete non-terminated executions") @Nullable @QueryValue(defaultValue = "false") Boolean includeNonTerminated,
@Parameter(description = "Whether to delete execution logs") @QueryValue(defaultValue = "true") Boolean deleteLogs,
@Parameter(description = "Whether to delete execution metrics") @QueryValue(defaultValue = "true") Boolean deleteMetrics,
@Parameter(description = "Whether to delete execution files in the internal storage") @QueryValue(defaultValue = "true") Boolean deleteStorage
@Parameter(description = "Whether to delete execution logs", required = false) @QueryValue(defaultValue = "true") Boolean deleteLogs,
@Parameter(description = "Whether to delete execution metrics", required = false) @QueryValue(defaultValue = "true") Boolean deleteMetrics,
@Parameter(description = "Whether to delete execution files in the internal storage", required = false) @QueryValue(defaultValue = "true") Boolean deleteStorage
) throws IOException {
List<Execution> executions = new ArrayList<>();
Set<ManualConstraintViolation<String>> invalids = new HashSet<>();
@@ -417,27 +420,27 @@ public class ExecutionController {
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Executions"}, summary = "Delete executions filter by query parameters")
public HttpResponse<?> deleteExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Parameter(description = "Whether to delete non-terminated executions") @Nullable @QueryValue(defaultValue = "false") Boolean includeNonTerminated,
@Parameter(description = "Whether to delete execution logs") @QueryValue(defaultValue = "true") Boolean deleteLogs,
@Parameter(description = "Whether to delete execution metrics") @QueryValue(defaultValue = "true") Boolean deleteMetrics,
@Parameter(description = "Whether to delete execution files in the internal storage") @QueryValue(defaultValue = "true") Boolean deleteStorage
@Parameter(description = "Whether to delete execution logs", required = false) @QueryValue(defaultValue = "true") Boolean deleteLogs,
@Parameter(description = "Whether to delete execution metrics", required = false) @QueryValue(defaultValue = "true") Boolean deleteMetrics,
@Parameter(description = "Whether to delete execution files in the internal storage", required = false) @QueryValue(defaultValue = "true") Boolean deleteStorage
) throws IOException {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -666,7 +669,16 @@ public class ExecutionController {
@ExecuteOn(TaskExecutors.IO)
@Post(uri = "/{namespace}/{id}", consumes = MediaType.MULTIPART_FORM_DATA)
@Operation(tags = {"Executions"}, summary = "Create a new execution for a flow")
@Operation(
tags = {"Executions"},
summary = "Create a new execution for a flow",
extensions = @Extension(
name = "x-sdk-customization",
properties = {
@ExtensionProperty(name = "x-multipart", value = "true")
}
)
)
@ApiResponse(responseCode = "409", description = "if the flow is disabled")
@ApiResponse(responseCode = "200", description = "On execution created", content = {@Content(schema = @Schema(implementation = ExecutionResponse.class))})
@SingleResult
@@ -996,22 +1008,22 @@ public class ExecutionController {
@Post(uri = "/restart/by-query")
@Operation(tags = {"Executions"}, summary = "Restart executions filter by query parameters")
public HttpResponse<?> restartExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
) throws Exception {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -1056,13 +1068,32 @@ public class ExecutionController {
@ExecuteOn(TaskExecutors.IO)
@Post(uri = "/{executionId}/replay-with-inputs", consumes = MediaType.MULTIPART_FORM_DATA)
@Operation(tags = {"Executions"}, summary = "Create a new execution from an old one and start it from a specified task run id")
@Operation(
tags = {"Executions"},
summary = "Create a new execution from an old one and start it from a specified task run id",
extensions = @Extension(
name = "x-sdk-customization",
properties = {
@ExtensionProperty(name = "x-multipart", value = "true")
}
)
)
public Mono<Execution> replayExecutionWithinputs(
@Parameter(description = "the original execution id to clone") @PathVariable String executionId,
@Parameter(description = "The taskrun id") @Nullable @QueryValue String taskRunId,
@Parameter(description = "The flow revision to use for new execution") @Nullable @QueryValue Integer revision,
@Parameter(description = "Set a list of breakpoints at specific tasks 'id.value', separated by a coma.") @QueryValue Optional<String> breakpoints,
@RequestBody(description = "The inputs") @Body MultipartBody inputs
@RequestBody(
description = "The inputs (multipart map)",
content = @Content(
mediaType = MediaType.MULTIPART_FORM_DATA,
schema = @Schema(
type = "object",
additionalProperties = Schema.AdditionalPropertiesValue.TRUE,
additionalPropertiesSchema = Object.class
)
)
) @Body MultipartBody inputs
) {
Optional<Execution> execution = executionRepository.findById(tenantService.resolveTenant(), executionId);
if (execution.isEmpty()) {
@@ -1239,22 +1270,22 @@ public class ExecutionController {
@ApiResponse(responseCode = "200", description = "On success", content = {@Content(schema = @Schema(implementation = BulkResponse.class))})
@ApiResponse(responseCode = "422", description = "Changed state with errors", content = {@Content(schema = @Schema(implementation = BulkErrorResponse.class))})
public HttpResponse<?> updateExecutionsStatusByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Parameter(description = "The new state of the executions") @NotNull @QueryValue State.Type newStatus
) throws QueueException {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
@@ -1302,7 +1333,7 @@ public class ExecutionController {
if (execution.getState().isTerminated() && !isOnKillCascade) {
throw new IllegalStateException("Execution is already finished, can't kill it");
}
eventPublisher.publishEvent(CrudEvent.of(execution, execution.withState(State.Type.KILLING)));
killQueue.emit(ExecutionKilledExecution
.builder()
@@ -1512,22 +1543,22 @@ public class ExecutionController {
@Post(uri = "/resume/by-query")
@Operation(tags = {"Executions"}, summary = "Resume executions filter by query parameters")
public HttpResponse<?> resumeExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
) throws Exception {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -1621,22 +1652,22 @@ public class ExecutionController {
@Post(uri = "/pause/by-query")
@Operation(tags = {"Executions"}, summary = "Pause executions filter by query parameters")
public HttpResponse<?> pauseExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
) throws Exception {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -1665,22 +1696,22 @@ public class ExecutionController {
@Delete(uri = "/kill/by-query")
@Operation(tags = {"Executions"}, summary = "Kill executions filter by query parameters")
public HttpResponse<?> killExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
) throws QueueException {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -1709,22 +1740,22 @@ public class ExecutionController {
@Post(uri = "/replay/by-query")
@Operation(tags = {"Executions"}, summary = "Create new executions from old ones filter by query parameters. Keep the flow revision")
public HttpResponse<?> replayExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Parameter(description = "If latest revision should be used") @Nullable @QueryValue(defaultValue = "false") Boolean latestRevision
) throws Exception {
@@ -1800,7 +1831,17 @@ public class ExecutionController {
@ExecuteOn(TaskExecutors.IO)
@Get(uri = "/{executionId}/follow", produces = MediaType.TEXT_EVENT_STREAM)
@Operation(tags = {"Executions"}, summary = "Follow an execution")
@Operation(
tags = {"Executions"},
summary = "Follow an execution",
extensions = @Extension(
name = "x-sdk-customization",
properties = {
@ExtensionProperty(name = "x-replace-follow-execution", value = "true"),
@ExtensionProperty(name = "x-skipped", value = "true")
}
)
)
public Flux<Event<Execution>> followExecution(
@Parameter(description = "The execution id") @PathVariable String executionId
) {
@@ -2017,22 +2058,22 @@ public class ExecutionController {
@Post(uri = "/labels/by-query")
@Operation(tags = {"Executions"}, summary = "Set label on executions filter by query parameters")
public HttpResponse<?> setLabelsOnTerminatedExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@RequestBody(description = "The labels to add to the execution") @Body @NotNull @Valid List<Label> setLabels
) {
@@ -2134,22 +2175,22 @@ public class ExecutionController {
@Post(uri = "/unqueue/by-query")
@Operation(tags = {"Executions"}, summary = "Unqueue executions filter by query parameters")
public HttpResponse<?> unqueueExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
@Parameter(description = "The new state of the unqueued executions") @Nullable @QueryValue State.Type newState
) throws Exception {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
@@ -2248,22 +2289,22 @@ public class ExecutionController {
@Post(uri = "/force-run/by-query")
@Operation(tags = {"Executions"}, summary = "Force run executions filter by query parameters")
public HttpResponse<?> forceRunExecutionsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
}) @Nullable @QueryValue Duration timeRange,
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
) throws Exception {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -2338,7 +2379,17 @@ public class ExecutionController {
@ExecuteOn(TaskExecutors.IO)
@Get(uri = "/{executionId}/follow-dependencies", produces = MediaType.TEXT_EVENT_STREAM)
@Operation(tags = {"Executions"}, summary = "Follow all execution dependencies executions")
@Operation(
tags = {"Executions"},
summary = "Follow all execution dependencies executions",
extensions = @Extension(
name = "x-sdk-customization",
properties = {
@ExtensionProperty(name = "x-replace-follow-dependencies-execution", value = "true"),
@ExtensionProperty(name = "x-skipped", value = "true")
}
)
)
public Flux<Event<ExecutionStatusEvent>> followDependenciesExecutions(
@Parameter(description = "The execution id") @PathVariable String executionId,
@Parameter(description = "If true, list only destination dependencies, otherwise list also source dependencies") @QueryValue(defaultValue = "false") boolean destinationOnly,

View File

@@ -46,6 +46,7 @@ import io.micronaut.validation.Validated;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
@@ -222,7 +223,7 @@ public class FlowController {
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
// Deprecated params
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
@@ -277,7 +278,7 @@ public class FlowController {
*/
@ExecuteOn(TaskExecutors.IO)
@Post(consumes = MediaType.ALL)
@Operation(tags = {"Flows"}, summary = "Create a flow from json object", deprecated = true)
@Operation(tags = {"Flows"}, summary = "Create a flow from json object", deprecated = true, hidden = true)
@Deprecated(forRemoval = true, since = "0.18")
@Hidden // we hide it otherwise this is the one that will be included in the OpenAPI spec instead of the YAML one.
public HttpResponse<Flow> createFlowFromJson(
@@ -334,7 +335,8 @@ public class FlowController {
summary = "Update a complete namespace from json object",
description = "All flow will be created / updated for this namespace.\n" +
"Flow that already created but not in `flows` will be deleted if the query delete is `true`",
deprecated = true
deprecated = true,
hidden = true
)
@Deprecated(forRemoval = true, since = "0.18")
@Hidden // we hide it otherwise this is the one that will be included in the OpenAPI spec instead of the YAML one.
@@ -437,7 +439,7 @@ public class FlowController {
@Put(uri = "{namespace}/{id}", consumes = MediaType.APPLICATION_YAML)
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Flows"}, summary = "Update a flow")
@Operation(tags = {"Flows"}, summary = "Update a flow")// force deprecated = false otherwise it is marked as deprecated, dont know why
@ApiResponse(responseCode = "200", description = "On success", content = {@Content(schema = @Schema(implementation = FlowWithSource.class))})
public HttpResponse<FlowWithSource> updateFlow(
@Parameter(description = "The flow namespace") @PathVariable String namespace,
@@ -476,9 +478,9 @@ public class FlowController {
/**
* @deprecated use {@link #updateFlow(String, String, String)} instead
*/
@Put(uri = "{namespace}/{id}", consumes = MediaType.ALL)
@Put(uri = "{namespace}/{id}", consumes = MediaType.APPLICATION_JSON)
@ExecuteOn(TaskExecutors.IO)
@Operation(tags = {"Flows"}, summary = "Update a flow", deprecated = true)
@Operation(tags = {"Flows"}, operationId = "updateFlowFromJson", summary = "Update a flow", deprecated = true, hidden = true)
@Deprecated(forRemoval = true, since = "0.18")
@Hidden // we hide it otherwise this is the one that will be included in the OpenAPI spec instead of the JSON one.
public HttpResponse<Flow> updateFlowFromJson(
@@ -666,7 +668,7 @@ public class FlowController {
@Post(uri = "/validate/task", consumes = MediaType.APPLICATION_YAML)
@Operation(tags = {"Flows"}, summary = "Validate a task")
public ValidateConstraintViolation validateTask(
@RequestBody(description = "A task definition that can be from tasks or triggers") @Body String task,
@RequestBody(description = "A task definition that can be from tasks or triggers") @Schema(implementation = Object.class) @Body String task,
@Parameter(description = "The type of task") @QueryValue TaskValidationType section
) {
ValidateConstraintViolation.ValidateConstraintViolationBuilder<?, ?> validateConstraintViolationBuilder = ValidateConstraintViolation.builder();
@@ -703,12 +705,12 @@ public class FlowController {
summary = "Export flows as a ZIP archive of yaml sources."
)
public HttpResponse<byte[]> exportFlowsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
) throws IOException {
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
@@ -741,12 +743,12 @@ public class FlowController {
summary = "Delete flows returned by the query parameters."
)
public HttpResponse<BulkResponse> deleteFlowsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
) {
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
@@ -784,12 +786,12 @@ public class FlowController {
summary = "Disable flows returned by the query parameters."
)
public HttpResponse<BulkResponse> disableFlowsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
) {
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
@@ -816,12 +818,12 @@ public class FlowController {
summary = "Enable flows returned by the query parameters."
)
public HttpResponse<BulkResponse> enableFlowsByQuery(
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
) {
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);

View File

@@ -22,8 +22,8 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.inject.Inject;
import java.io.*;
import java.time.*;
import java.io.IOException;
import java.time.Duration;
import java.util.*;
@Validated
@@ -91,7 +91,7 @@ public class KVController {
}
@ExecuteOn(TaskExecutors.IO)
@Put(uri = "{key}", consumes = {MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Put(uri = "{key}", consumes = {MediaType.TEXT_PLAIN})
@Operation(tags = {"KV"}, summary = "Puts a key-value pair in store")
public void setKeyValue(
HttpHeaders httpHeaders,

View File

@@ -27,6 +27,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.validation.Validated;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import jakarta.inject.Inject;
import jakarta.validation.constraints.Min;
import org.slf4j.event.Level;
@@ -66,7 +67,7 @@ public class LogController {
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
@Parameter(description = "Filters") @Nullable @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @Nullable @QueryFilterFormat List<QueryFilter> filters,
// Deprecated params
@Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Parameter(description = "A namespace filter prefix",deprecated = true) @Nullable @QueryValue String namespace,

View File

@@ -19,6 +19,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.validation.Validated;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import jakarta.inject.Inject;
import java.io.IOException;
@@ -44,7 +45,7 @@ public class NamespaceSecretController {
@Parameter(description = "The current page") @QueryValue(value = "page", defaultValue = "1") int page,
@Parameter(description = "The current page size") @QueryValue(value = "size", defaultValue = "10") int size,
@Parameter(description = "The sort of current page") @Nullable @QueryValue(value = "sort") List<String> sort,
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters
) throws IllegalArgumentException, IOException {
final String tenantId = this.tenantService.resolveTenant();
List<String> items = secretService.inheritedSecrets(tenantId, namespace).get(namespace).stream().toList();

View File

@@ -20,6 +20,7 @@ import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import jakarta.inject.Inject;
import jakarta.validation.constraints.Min;
@@ -44,7 +45,7 @@ public class TaskRunController {
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
// Deprecated params
@Parameter(description = "A string filter",deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,

View File

@@ -32,6 +32,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.validation.Validated;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
@@ -82,7 +83,7 @@ public class TriggerController {
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
// Deprecated params
@Parameter(description = "A string filter",deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@@ -205,10 +206,10 @@ public class TriggerController {
@Post(uri = "/unlock/by-query")
@Operation(tags = {"Triggers"}, summary = "Unlock triggers by query parameters")
public MutableHttpResponse<?> unlockTriggersByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
) {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -280,13 +281,13 @@ public class TriggerController {
if (abstractTrigger == null) {
throw new HttpStatusException(HttpStatus.NOT_FOUND, String.format("Flow %s has no trigger %s", newTrigger.getFlowId(), newTrigger.getTriggerId()));
}
if (abstractTrigger instanceof RealtimeTriggerInterface) {
throw new IllegalArgumentException("Realtime triggers can not be updated through the API, please edit the trigger from the flow.");
}
Trigger updatedTrigger;
if (newTrigger.getBackfill() != null) {
try {
updatedTrigger = setTriggerBackfill(newTrigger, maybeFlow.get(), abstractTrigger);
@@ -296,13 +297,13 @@ public class TriggerController {
} else {
updatedTrigger = setTriggerDisabled(newTrigger.uid(), newTrigger.getDisabled(), abstractTrigger, maybeFlow.get());
}
if (updatedTrigger == null) {
return HttpResponse.notFound();
}
return HttpResponse.ok(updatedTrigger);
}
@ExecuteOn(TaskExecutors.IO)
@Post(uri = "/{namespace}/{flowId}/{triggerId}/restart")
@Operation(tags = {"Triggers"}, summary = "Restart a trigger")
@@ -369,10 +370,10 @@ public class TriggerController {
@Post(uri = "/backfill/pause/by-query")
@Operation(tags = {"Triggers"}, summary = "Pause backfill for given triggers")
public MutableHttpResponse<?> pauseBackfillByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
) throws QueueException {
// Updating the backfill within the flux does not works
List<Trigger> triggers = triggerRepository
@@ -408,10 +409,10 @@ public class TriggerController {
@Post(uri = "/backfill/unpause/by-query")
@Operation(tags = {"Triggers"}, summary = "Unpause backfill for given triggers")
public MutableHttpResponse<?> unpauseBackfillByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
) throws QueueException {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -477,10 +478,10 @@ public class TriggerController {
@Post(uri = "/backfill/delete/by-query")
@Operation(tags = {"Triggers"}, summary = "Delete backfill for given triggers")
public MutableHttpResponse<?> deleteBackfillByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
) throws QueueException {
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
filters,
@@ -521,10 +522,10 @@ public class TriggerController {
@Post(uri = "/set-disabled/by-query")
@Operation(tags = {"Triggers"}, summary = "Disable/enable triggers by query parameters")
public MutableHttpResponse<?> disabledTriggersByQuery(
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
@Parameter(description = "The disabled state") @QueryValue(defaultValue = "true") Boolean disabled
) throws QueueException {
@@ -557,24 +558,24 @@ public class TriggerController {
public void setTriggerDisabled(Trigger trigger, Boolean disabled) throws QueueException {
Optional<Flow> maybeFlow = this.flowRepository.findById(this.tenantService.resolveTenant(), trigger.getNamespace(), trigger.getFlowId());
if (maybeFlow.isEmpty()) {
return; // Flow doesn't exist
}
Optional<AbstractTrigger> maybeAbstractTrigger = maybeFlow.flatMap(flow -> flow.getTriggers().stream().filter(t -> t.getId().equals(trigger.getTriggerId())).findFirst());
if (maybeAbstractTrigger.isEmpty()) {
return; // Trigger doesn't exist
}
if (maybeAbstractTrigger.get() instanceof RealtimeTriggerInterface) {
return; // RealTimeTriggers can't be disabled/enabled through API.
}
setTriggerDisabled(trigger.uid(), disabled, maybeAbstractTrigger.get(), maybeFlow.get());
}
private Trigger setTriggerDisabled(String triggerUID, Boolean disabled, AbstractTrigger triggerDefinition, Flow flow) throws QueueException {
return this.triggerRepository.lock(triggerUID, throwFunction(current -> {
if (disabled.equals(current.getDisabled())) {
@@ -583,46 +584,46 @@ public class TriggerController {
return doSetTriggerDisabled(current, disabled, flow, triggerDefinition);
}));
}
private Trigger setTriggerBackfill(Trigger newTrigger, Flow flow, AbstractTrigger abstractTrigger) throws Exception {
return this.triggerRepository.lock(newTrigger.uid(), throwFunction(current -> doSetTriggerBackfill(current, newTrigger.getBackfill(), flow, abstractTrigger)));
}
protected Trigger doSetTriggerDisabled(Trigger currentState, Boolean disabled, Flow flow, AbstractTrigger trigger) throws QueueException {
Trigger.TriggerBuilder<?, ?> builder = currentState.toBuilder().disabled(disabled);
if (disabled) {
builder = builder.nextExecutionDate(null);
}
Trigger updated = builder.build();
triggerQueue.emit(updated);
return updated;
}
protected Trigger doSetTriggerBackfill(Trigger currentState, Backfill backfill, Flow flow, AbstractTrigger trigger) throws Exception {
Trigger updated;
ZonedDateTime nextExecutionDate = null;
RunContext runContext = runContextFactory.of(flow, trigger);
ConditionContext conditionContext = conditionService.conditionContext(runContext, flow, null);
// We must set up the backfill before the update to calculate the next execution date
updated = currentState.withBackfill(backfill);
if (trigger instanceof PollingTriggerInterface pollingTriggerInterface) {
nextExecutionDate = pollingTriggerInterface.nextEvaluationDate(conditionContext, Optional.of(updated));
}
updated = updated
.toBuilder()
.nextExecutionDate(nextExecutionDate)
.build();
triggerQueue.emit(updated);
return updated;
}
public int backfillsAction(List<Trigger> triggers, BACKFILL_ACTION action) throws QueueException {
AtomicInteger count = new AtomicInteger();
triggers.forEach(throwConsumer(trigger -> {

View File

@@ -1294,13 +1294,14 @@ class ExecutionControllerRunnerTest {
Execution pausedExecution = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
assertThat(pausedExecution.getState().isPaused()).isTrue();
// resume the execution
HttpResponse<?> resumeResponse = client.toBlocking().exchange(
// kill the execution
HttpResponse<?> killResponse = client.toBlocking().exchange(
HttpRequest.DELETE("/api/v1/main/executions/" + pausedExecution.getId() + "/kill"));
assertThat(resumeResponse.getStatus().getCode()).isEqualTo(HttpStatus.ACCEPTED.getCode());
assertThat(killResponse.getStatus().getCode()).isEqualTo(HttpStatus.ACCEPTED.getCode());
// check that the execution is no more paused
awaitExecution(pausedExecution.getId(), exec -> !exec.getState().isPaused());
// check that the execution is killed
Execution killedExecution = awaitExecution(pausedExecution.getId(), exec -> exec.getState().getCurrent().isKilled());
assertThat(killedExecution.getTaskRunList()).hasSize(1);
}
// This test is flaky on CI as the flow may be already SUCCESS when we kill it if CI is super slow

View File

@@ -1,9 +1,5 @@
package io.kestra.webserver.controllers.api;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.within;
import io.kestra.core.exceptions.ResourceExpiredException;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.kv.KVType;
@@ -23,6 +19,13 @@ import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.reactor.http.client.ReactorHttpClient;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigDecimal;
@@ -36,12 +39,9 @@ import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.within;
@KestraTest(resolveParameters = false)
class KVControllerTest {
@@ -163,24 +163,24 @@ class KVControllerTest {
static Stream<Arguments> kvSetKeyValueArgs() {
return Stream.of(
Arguments.of(MediaType.APPLICATION_JSON, "{\"hello\":\"world\"}", Map.class),
Arguments.of(MediaType.APPLICATION_JSON, "[\"hello\",\"world\"]", List.class),
Arguments.of(MediaType.APPLICATION_JSON, "\"hello\"", String.class),
Arguments.of(MediaType.APPLICATION_JSON, "1", Integer.class),
Arguments.of(MediaType.APPLICATION_JSON, "1.0", BigDecimal.class),
Arguments.of(MediaType.APPLICATION_JSON, "true", Boolean.class),
Arguments.of(MediaType.APPLICATION_JSON, "false", Boolean.class),
Arguments.of(MediaType.APPLICATION_JSON, "2021-09-01", LocalDate.class),
Arguments.of(MediaType.APPLICATION_JSON, "2021-09-01T01:02:03Z", Instant.class),
Arguments.of(MediaType.APPLICATION_JSON, "\"PT5S\"", Duration.class)
Arguments.of("{\"hello\":\"world\"}", Map.class),
Arguments.of("[\"hello\",\"world\"]", List.class),
Arguments.of("\"hello\"", String.class),
Arguments.of("1", Integer.class),
Arguments.of("1.0", BigDecimal.class),
Arguments.of("true", Boolean.class),
Arguments.of("false", Boolean.class),
Arguments.of("2021-09-01", LocalDate.class),
Arguments.of("2021-09-01T01:02:03Z", Instant.class),
Arguments.of("\"PT5S\"", Duration.class)
);
}
@ParameterizedTest
@MethodSource("kvSetKeyValueArgs")
void setKeyValue(MediaType mediaType, String value, Class<?> expectedClass) throws IOException, ResourceExpiredException {
void setKeyValue(String value, Class<?> expectedClass) throws IOException, ResourceExpiredException {
String myDescription = "myDescription";
client.toBlocking().exchange(HttpRequest.PUT("/api/v1/main/namespaces/" + NAMESPACE + "/kv/my-key", value).contentType(mediaType).header("ttl", "PT5M").header("description", myDescription));
client.toBlocking().exchange(HttpRequest.PUT("/api/v1/main/namespaces/" + NAMESPACE + "/kv/my-key", value).header("ttl", "PT5M").header("description", myDescription));
KVStore kvStore = new InternalKVStore(MAIN_TENANT, NAMESPACE, storageInterface);
Class<?> valueClazz = kvStore.getValue("my-key").get().value().getClass();
@@ -256,7 +256,7 @@ class KVControllerTest {
assertThat(httpClientResponseException.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
assertThat(httpClientResponseException.getMessage()).isEqualTo(expectedErrorMessage);
httpClientResponseException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.PUT("/api/v1/main/namespaces/" + NAMESPACE + "/kv/bad$key", "\"content\"").contentType(MediaType.APPLICATION_JSON)));
httpClientResponseException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.PUT("/api/v1/main/namespaces/" + NAMESPACE + "/kv/bad$key", "\"content\"")));
assertThat(httpClientResponseException.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
assertThat(httpClientResponseException.getMessage()).isEqualTo(expectedErrorMessage);