Compare commits

...

59 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
github-actions[bot]
483f7dc3b2 chore(version): update to version '1.0.7' 2025-10-21 12:03:05 +00:00
Piyush Bhaskar
3c2da63837 fix(core): handle 404 error in kv retrieval with message (#12191) 2025-10-21 15:19:47 +05:30
Nicolas K.
31527891b2 feat(flows): add truncate parameter for log shipper (#12131)
Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-10-21 11:06:51 +02:00
Roman Acevedo
6364f419d9 fix(flows): allow using OSS CLI to deploy EE flows
- fixes https://github.com/kestra-io/kestra-ee/issues/5490
2025-10-21 09:33:15 +02:00
Irfan
3c14432412 feat(plugins): enhance documentation request handling to prevent unnecessary reloads (#11911)
Co-authored-by: Barthélémy Ledoux <ledouxb@me.com>
Co-authored-by: iitzIrFan <irfanlhawk@gmail.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
Co-authored-by: Bart Ledoux <bledoux@kestra.io>
2025-10-17 11:48:25 +02:00
YannC
eaea4f5012 Fix/validate endpoint fix (#12121)
* fix: validateTask & validateTrigger endpoint changes for SDK

* fix: validateTask & validateTrigger endpoint changes for SDK
2025-10-17 11:12:18 +02:00
Roman Acevedo
d43390a579 fix(flows): allow using OSS CLI to validate EE flows (#12104)
* fix(flows): allow using OSS CLI to validate EE flows

https://github.com/kestra-io/kestra/pull/12047 was not enough

- fixxes https://github.com/kestra-io/kestra-ee/issues/5455

* f
2025-10-16 19:34:02 +02:00
Roman Acevedo
2404c36d35 fix(flows): allow using OSS CLI to validate EE flows
- fixes https://github.com/kestra-io/kestra-ee/issues/5455
2025-10-16 18:55:39 +02:00
Miloš Paunović
bdbd217171 fix(iam): prevent infinite loop when permissions are missing while loading custom blueprints (#12092)
Closes https://github.com/kestra-io/kestra-ee/issues/5405.
2025-10-16 14:39:05 +02:00
brian-mulier-p
019c16af3c feat(ai): add PEM Certificate handling to GeminiAiService (#11739)
closes kestra-io/kestra-ee#5342
2025-10-15 14:13:19 +02:00
Hemant M Mehta
ff7d7c6a0b fix(executions): properly handle filename with special chars (#11814)
* fix: artifact-filename-validation

closes: #10802

* fix: test

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

* fix: test

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

* fix: test

* fix(core): use deterministic file naming in FilesService

---------

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>
2025-10-15 09:28:53 +02:00
github-actions[bot]
1042be87da chore(version): update to version '1.0.6' 2025-10-14 12:30:55 +00:00
brian-mulier-p
104805d780 fix(flows): pebble autocompletion performance optimization (#11981)
closes #11881
2025-10-14 11:37:46 +02:00
YannC
33c8e54f36 Fix: openapi tweaks (#11929)
* fix: added some on @ApiResponse annotation + added nullable annotation for TaskRun class

* fix: review changes
2025-10-13 18:05:38 +02:00
nKwiatkowski
ff2e00d1ca feat(tests): add flaky tests handling 2025-10-13 17:06:28 +02:00
brian-mulier-p
0fe3f317c7 feat(runners): add syncWorkingDirectory property to remote task runners (#11945)
part of kestra-io/kestra-ee#4761
2025-10-13 11:35:52 +02:00
brian-mulier-p
f753d15c91 feat(runners): add syncWorkingDirectory property to remote task runners (#11602)
part of kestra-io/kestra-ee#4761
2025-10-13 11:35:52 +02:00
brian-mulier-p
c03e31de68 fix(ai): remove thoughts return from AI Copilot (#11935)
closes kestra-io/kestra-ee#5422
2025-10-13 09:56:11 +02:00
Miloš Paunović
9a79f9a64c feat(flows): save editor panel layout after creation (#11276)
Closes https://github.com/kestra-io/kestra/issues/9887.

Co-authored-by: Bart Ledoux <bledoux@kestra.io>
2025-10-10 12:59:11 +02:00
99 changed files with 1963 additions and 872 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

@@ -205,23 +205,59 @@ subprojects {
testImplementation 'org.assertj:assertj-core'
}
test {
useJUnitPlatform()
def commonTestConfig = { Test t ->
// set Xmx for test workers
maxHeapSize = '4g'
t.maxHeapSize = '4g'
// configure en_US default locale for tests
systemProperty 'user.language', 'en'
systemProperty 'user.country', 'US'
t.systemProperty 'user.language', 'en'
t.systemProperty 'user.country', 'US'
environment 'SECRET_MY_SECRET', "{\"secretKey\":\"secretValue\"}".bytes.encodeBase64().toString()
environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n"
environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString()
environment 'SECRET_NON_B64_SECRET', "some secret value"
environment 'SECRET_PASSWORD', "cGFzc3dvcmQ="
environment 'ENV_TEST1', "true"
environment 'ENV_TEST2', "Pass by env"
t.environment 'SECRET_MY_SECRET', "{\"secretKey\":\"secretValue\"}".bytes.encodeBase64().toString()
t.environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n"
t.environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString()
t.environment 'SECRET_NON_B64_SECRET', "some secret value"
t.environment 'SECRET_PASSWORD', "cGFzc3dvcmQ="
t.environment 'ENV_TEST1', "true"
t.environment 'ENV_TEST2', "Pass by env"
}
tasks.register('flakyTest', Test) { Test t ->
group = 'verification'
description = 'Runs tests tagged @Flaky but does not fail the build.'
useJUnitPlatform {
includeTags 'flaky'
}
ignoreFailures = true
reports {
junitXml.required = true
junitXml.outputPerTestCase = true
junitXml.mergeReruns = true
junitXml.includeSystemErrLog = true
junitXml.outputLocation = layout.buildDirectory.dir("test-results/flakyTest")
}
commonTestConfig(t)
}
test {
useJUnitPlatform {
excludeTags 'flaky'
}
reports {
junitXml.required = true
junitXml.outputPerTestCase = true
junitXml.mergeReruns = true
junitXml.includeSystemErrLog = true
junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
}
commonTestConfig(it)
finalizedBy(tasks.named('flakyTest'))
}
testlogger {

View File

@@ -117,7 +117,7 @@ public abstract class AbstractValidateCommand extends AbstractApiCommand {
try(DefaultHttpClient client = client()) {
MutableHttpRequest<String> request = HttpRequest
.POST(apiUri("/flows/validate", tenantService.getTenantId(tenantId)), body).contentType(MediaType.APPLICATION_YAML);
.POST(apiUri("/flows/validate", tenantService.getTenantIdAndAllowEETenants(tenantId)), body).contentType(MediaType.APPLICATION_YAML);
List<ValidateConstraintViolation> validations = client.toBlocking().retrieve(
this.requestOptions(request),

View File

@@ -24,7 +24,8 @@ public class FlowValidateCommand extends AbstractValidateCommand {
private FlowService flowService;
@Inject
private TenantIdSelectorService tenantService;
private TenantIdSelectorService tenantIdSelectorService;
@Override
public Integer call() throws Exception {
@@ -39,7 +40,7 @@ public class FlowValidateCommand extends AbstractValidateCommand {
FlowWithSource flow = (FlowWithSource) object;
List<String> warnings = new ArrayList<>();
warnings.addAll(flowService.deprecationPaths(flow).stream().map(deprecation -> deprecation + " is deprecated").toList());
warnings.addAll(flowService.warnings(flow, tenantService.getTenantId(tenantId)));
warnings.addAll(flowService.warnings(flow, tenantIdSelectorService.getTenantIdAndAllowEETenants(tenantId)));
return warnings;
},
(Object object) -> {

View File

@@ -64,7 +64,7 @@ public class FlowNamespaceUpdateCommand extends AbstractServiceNamespaceUpdateCo
}
try(DefaultHttpClient client = client()) {
MutableHttpRequest<String> request = HttpRequest
.POST(apiUri("/flows/", tenantService.getTenantId(tenantId)) + namespace + "?delete=" + delete, body).contentType(MediaType.APPLICATION_YAML);
.POST(apiUri("/flows/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "?delete=" + delete, body).contentType(MediaType.APPLICATION_YAML);
List<UpdateResult> updated = client.toBlocking().retrieve(
this.requestOptions(request),

View File

@@ -49,7 +49,7 @@ public class NamespaceFilesUpdateCommand extends AbstractApiCommand {
try (var files = Files.walk(from); DefaultHttpClient client = client()) {
if (delete) {
client.toBlocking().exchange(this.requestOptions(HttpRequest.DELETE(apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/files?path=" + to, null)));
client.toBlocking().exchange(this.requestOptions(HttpRequest.DELETE(apiUri("/namespaces/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "/files?path=" + to, null)));
}
KestraIgnore kestraIgnore = new KestraIgnore(from);
@@ -67,7 +67,7 @@ public class NamespaceFilesUpdateCommand extends AbstractApiCommand {
client.toBlocking().exchange(
this.requestOptions(
HttpRequest.POST(
apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/files?path=" + destination,
apiUri("/namespaces/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "/files?path=" + destination,
body
).contentType(MediaType.MULTIPART_FORM_DATA)
)

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

@@ -49,7 +49,7 @@ public class TemplateNamespaceUpdateCommand extends AbstractServiceNamespaceUpda
try (DefaultHttpClient client = client()) {
MutableHttpRequest<List<Template>> request = HttpRequest
.POST(apiUri("/templates/", tenantService.getTenantId(tenantId)) + namespace + "?delete=" + delete, templates);
.POST(apiUri("/templates/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "?delete=" + delete, templates);
List<UpdateResult> updated = client.toBlocking().retrieve(
this.requestOptions(request),

View File

@@ -16,4 +16,11 @@ public class TenantIdSelectorService {
}
return MAIN_TENANT;
}
public String getTenantIdAndAllowEETenants(String tenantId) {
if (StringUtils.isNotBlank(tenantId)){
return tenantId;
}
return MAIN_TENANT;
}
}

View File

@@ -27,6 +27,26 @@ class FlowValidateCommandTest {
}
}
@Test
// github action kestra-io/validate-action requires being able to validate Flows from OSS CLI against a remote EE instance
void runForEEInstance() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
String[] args = {
"--tenant",
"some-ee-tenant",
"--local",
"src/test/resources/helper/include.yaml"
};
Integer call = PicocliRunner.call(FlowValidateCommand.class, ctx, args);
assertThat(call).isZero();
assertThat(out.toString()).contains("✓ - io.kestra.cli / include");
}
}
@Test
void warning() {
ByteArrayOutputStream out = new ByteArrayOutputStream();

View File

@@ -7,6 +7,8 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
/**
* Top-level marker interface for Kestra's plugin of type App.
*/
@@ -18,6 +20,6 @@ public interface AppBlockInterface extends io.kestra.core.models.Plugin {
)
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
String getType();
}

View File

@@ -7,6 +7,8 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
/**
* Top-level marker interface for Kestra's plugin of type App.
*/
@@ -18,6 +20,6 @@ public interface AppPluginInterface extends io.kestra.core.models.Plugin {
)
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
String getType();
}

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

@@ -12,6 +12,8 @@ import lombok.experimental.SuperBuilder;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@io.kestra.core.models.annotations.Plugin
@SuperBuilder
@Getter
@@ -20,6 +22,6 @@ import jakarta.validation.constraints.Pattern;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public abstract class Condition implements Plugin, Rethrow.PredicateChecked<ConditionContext, InternalException> {
@NotNull
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
}

View File

@@ -20,6 +20,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@@ -28,7 +30,7 @@ import java.util.Set;
public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
private String type;
private Map<String, C> columns;

View File

@@ -19,6 +19,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@@ -27,7 +29,7 @@ import java.util.Set;
public abstract class DataFilterKPI<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
private String type;
private C columns;

View File

@@ -12,6 +12,8 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@@ -26,7 +28,7 @@ public abstract class Chart<P extends ChartOption> implements io.kestra.core.mod
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
@Valid

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

@@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Nullable;
import lombok.Builder;
import lombok.Value;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.event.Level;
import jakarta.validation.constraints.NotNull;
@@ -120,6 +121,16 @@ public class LogEntry implements DeletedInterface, TenantInterface {
return logEntry.getTimestamp().toString() + " " + logEntry.getLevel() + " " + logEntry.getMessage();
}
public static String toPrettyString(LogEntry logEntry, Integer maxMessageSize) {
String message;
if (maxMessageSize != null && maxMessageSize > 0) {
message = StringUtils.truncate(logEntry.getMessage(), maxMessageSize);
} else {
message = logEntry.getMessage();
}
return logEntry.getTimestamp().toString() + " " + logEntry.getLevel() + " " + message;
}
public Map<String, String> toMap() {
return Stream
.of(

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;
@@ -52,6 +53,8 @@ public class TaskRun implements TenantInterface {
@With
@JsonInclude(JsonInclude.Include.ALWAYS)
@Nullable
@Schema(implementation = Object.class)
Variables outputs;
@NotNull
@@ -64,7 +67,6 @@ public class TaskRun implements TenantInterface {
Boolean dynamic;
// Set it to true to force execution even if the execution is killed
@Nullable
@With
Boolean forceExecution;
@@ -217,7 +219,7 @@ public class TaskRun implements TenantInterface {
public boolean isSame(TaskRun taskRun) {
return this.getId().equals(taskRun.getId()) &&
((this.getValue() == null && taskRun.getValue() == null) || (this.getValue() != null && this.getValue().equals(taskRun.getValue()))) &&
((this.getIteration() == null && taskRun.getIteration() == null) || (this.getIteration() != null && this.getIteration().equals(taskRun.getIteration()))) ;
((this.getIteration() == null && taskRun.getIteration() == null) || (this.getIteration() != null && this.getIteration().equals(taskRun.getIteration())));
}
public String toString(boolean pretty) {
@@ -249,7 +251,7 @@ public class TaskRun implements TenantInterface {
* This method is used when the retry is apply on a task
* but the retry type is NEW_EXECUTION
*
* @param retry Contains the retry configuration
* @param retry Contains the retry configuration
* @param execution Contains the attempt number and original creation date
* @return The next retry date, null if maxAttempt || maxDuration is reached
*/
@@ -270,6 +272,7 @@ public class TaskRun implements TenantInterface {
/**
* This method is used when the Retry definition comes from the flow
*
* @param retry The retry configuration
* @return The next retry date, null if maxAttempt || maxDuration is reached
*/

View File

@@ -77,14 +77,6 @@ public abstract class AbstractFlow implements FlowInterface {
Map<String, Object> variables;
@Schema(
oneOf = {
String.class, // Corresponds to 'type: string' in OpenAPI
Map.class // Corresponds to 'type: object' in OpenAPI
}
)
interface StringOrMapValue {}
@Valid
private WorkerGroup workerGroup;

View File

@@ -24,10 +24,6 @@ public class PluginDefault {
@Schema(
type = "object",
oneOf = {
Map.class,
String.class
},
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
)
private final Map<String, Object> values;

View File

@@ -8,6 +8,8 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public interface TaskInterface extends Plugin, PluginVersioning {
@NotNull
@@ -17,7 +19,7 @@ public interface TaskInterface extends Plugin, PluginVersioning {
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
@Schema(title = "The class name of this task.")
String getType();
}

View File

@@ -11,6 +11,8 @@ import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import reactor.core.publisher.Flux;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@Plugin
@SuperBuilder(toBuilder = true)
@Getter
@@ -22,7 +24,7 @@ public abstract class LogExporter<T extends Output> implements io.kestra.core.m
protected String id;
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
public abstract T sendLogs(RunContext runContext, Flux<LogRecord> logRecords) throws Exception;

View File

@@ -8,12 +8,16 @@ public final class LogRecordMapper {
private LogRecordMapper(){}
public static LogRecord mapToLogRecord(LogEntry log) {
return mapToLogRecord(log, null);
}
public static LogRecord mapToLogRecord(LogEntry log, Integer maxMessageSize) {
return LogRecord.builder()
.resource("Kestra")
.timestampEpochNanos(instantInNanos(log.getTimestamp()))
.severity(log.getLevel().name())
.attributes(log.toLogMap())
.bodyValue(LogEntry.toPrettyString(log))
.bodyValue(LogEntry.toPrettyString(log, maxMessageSize))
.build();
}

View File

@@ -1,3 +1,11 @@
package io.kestra.core.models.tasks.runners;
public interface RemoteRunnerInterface {}
import io.kestra.core.models.property.Property;
import io.swagger.v3.oas.annotations.media.Schema;
public interface RemoteRunnerInterface {
@Schema(
title = "Whether to synchronize working directory from remote runner back to local one after run."
)
Property<Boolean> getSyncWorkingDirectory();
}

View File

@@ -30,6 +30,10 @@ public interface TaskCommands {
Map<String, Object> getAdditionalVars();
default String outputDirectoryName() {
return this.getWorkingDirectory().relativize(this.getOutputDirectory()).toString();
}
Path getWorkingDirectory();
Path getOutputDirectory();

View File

@@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
/**
* Base class for all task runners.
@@ -36,7 +37,7 @@ import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public abstract class TaskRunner<T extends TaskRunnerDetailResult> implements Plugin, PluginVersioning, WorkerJobLifecycle {
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)

View File

@@ -47,7 +47,6 @@ abstract public class AbstractTrigger implements TriggerInterface {
@Valid
protected List<@Valid @NotNull Condition> conditions;
@NotNull
@Builder.Default
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
@Schema(defaultValue = "false")

View File

@@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
public interface TriggerInterface extends Plugin, PluginVersioning {
@NotNull
@@ -17,7 +18,7 @@ public interface TriggerInterface extends Plugin, PluginVersioning {
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
@Schema(title = "The class name for this current trigger.")
String getType();

View File

@@ -8,6 +8,8 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@io.kestra.core.models.annotations.Plugin
@SuperBuilder(toBuilder = true)
@Getter
@@ -15,6 +17,6 @@ import lombok.experimental.SuperBuilder;
public abstract class AdditionalPlugin implements Plugin {
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
}

View File

@@ -82,6 +82,8 @@ public abstract class FilesService {
}
private static String resolveUniqueNameForFile(final Path path) {
return IdUtils.from(path.toString()) + "-" + path.toFile().getName();
String filename = path.getFileName().toString();
String encodedFilename = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8);
return IdUtils.from(path.toString()) + "-" + encodedFilename;
}
}

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

@@ -0,0 +1,5 @@
package io.kestra.core.utils;
public class RegexPatterns {
public static final String JAVA_IDENTIFIER_REGEX = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$";
}

View File

@@ -7,6 +7,8 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
@Getter
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
@@ -20,6 +22,6 @@ import lombok.Getter;
public class MarkdownSource {
@NotNull
@NotBlank
@Pattern(regexp = "^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
private String type;
}

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

@@ -45,6 +45,29 @@ public class LogRecordMapperTest {
softly.then(logRecord.getBodyValue()).isEqualTo("2011-12-03T10:15:30.123456789Z INFO message");
}
@Test
public void should_map_with_truncate(){
LogEntry logEntry = LogEntry.builder()
.tenantId("tenantId")
.namespace("namespace")
.flowId("flowId")
.taskId("taskId")
.executionId("executionId")
.taskRunId("taskRunId")
.attemptNumber(1)
.triggerId("triggerId")
.timestamp(Instant.parse("2011-12-03T10:15:30.123456789Z"))
.level(Level.INFO)
.thread("thread")
.message("message")
.build();
LogRecord logRecord = LogRecordMapper.mapToLogRecord(logEntry, 1);
assertThat(logRecord.getBodyValue()).isEqualTo("2011-12-03T10:15:30.123456789Z INFO m");
logRecord = LogRecordMapper.mapToLogRecord(logEntry, 0);
assertThat(logRecord.getBodyValue()).isEqualTo("2011-12-03T10:15:30.123456789Z INFO message");
}
@Test
public void should_convert_instant_in_nanos(){
Instant instant = Instant.parse("2011-12-03T10:15:30.123456789Z");

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

@@ -9,6 +9,7 @@ import io.micronaut.context.annotation.Property;
import jakarta.inject.Inject;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -97,6 +98,35 @@ class FilesServiceTest {
assertThat(outputs.size()).isEqualTo(1);
}
@Test
void testOutputFilesWithSpecialCharacters(@TempDir Path tempDir) throws Exception {
var runContext = runContextFactory.of();
Path fileWithSpace = tempDir.resolve("with space.txt");
Path fileWithUnicode = tempDir.resolve("สวัสดี.txt");
Files.writeString(fileWithSpace, "content");
Files.writeString(fileWithUnicode, "content");
Path targetFileWithSpace = runContext.workingDir().path().resolve("with space.txt");
Path targetFileWithUnicode = runContext.workingDir().path().resolve("สวัสดี.txt");
Files.copy(fileWithSpace, targetFileWithSpace);
Files.copy(fileWithUnicode, targetFileWithUnicode);
Map<String, URI> outputFiles = FilesService.outputFiles(
runContext,
List.of("with space.txt", "สวัสดี.txt")
);
assertThat(outputFiles).hasSize(2);
assertThat(outputFiles).containsKey("with space.txt");
assertThat(outputFiles).containsKey("สวัสดี.txt");
assertThat(runContext.storage().getFile(outputFiles.get("with space.txt"))).isNotNull();
assertThat(runContext.storage().getFile(outputFiles.get("สวัสดี.txt"))).isNotNull();
}
private URI createFile() throws IOException {
File tempFile = File.createTempFile("file", ".txt");
Files.write(tempFile.toPath(), "Hello World".getBytes());

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.5
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

@@ -4,11 +4,18 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
/**
* used to document that a test is flaky and needs to be reworked
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Tag("flaky")
public @interface FlakyTest {
/**
* Use to explain why the test is flaky
*/
String description() default "";
}

View File

@@ -173,18 +173,32 @@ public abstract class AbstractTaskRunnerTest {
var commands = initScriptCommands(runContext);
Mockito.when(commands.getEnableOutputDirectory()).thenReturn(false);
Mockito.when(commands.outputDirectoryEnabled()).thenReturn(false);
Mockito.when(commands.getCommands()).thenReturn(
Property.ofValue(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("echo 'Hello World' > file.txt")))
);
Mockito.when(commands.relativeWorkingDirectoryFilesPaths()).thenCallRealMethod();
Mockito.when(commands.relativeWorkingDirectoryFilesPaths(false)).thenCallRealMethod();
var taskRunner = taskRunner();
Property<List<String>> renderedCommands = Property.ofValue(ScriptService.replaceInternalStorage(
runContext,
taskRunner.additionalVars(runContext, commands),
ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("echo 'Hello World' > " + (needsToSpecifyWorkingDirectory() ? "{{workingDir}}/" : "") + "file.txt")),
taskRunner instanceof RemoteRunnerInterface
));
Mockito.when(commands.getCommands()).thenReturn(
renderedCommands
);
var result = taskRunner.run(runContext, commands, Collections.emptyList());
assertThat(result).isNotNull();
assertThat(result.getExitCode()).isZero();
renderedCommands = Property.ofValue(ScriptService.replaceInternalStorage(
runContext,
taskRunner.additionalVars(runContext, commands),
ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("cat " + (needsToSpecifyWorkingDirectory() ? "{{workingDir}}/" : "") + "file.txt")),
taskRunner instanceof RemoteRunnerInterface
));
Mockito.when(commands.getCommands()).thenReturn(
Property.ofValue(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("cat file.txt")))
renderedCommands
);
result = taskRunner.run(runContext, commands, Collections.emptyList());
@@ -249,6 +263,7 @@ public abstract class AbstractTaskRunnerTest {
var outputDirectory = workingDirectory.resolve(IdUtils.create());
outputDirectory.toFile().mkdirs();
Mockito.when(commands.getOutputDirectory()).thenReturn(outputDirectory);
Mockito.when(commands.outputDirectoryName()).thenCallRealMethod();
Mockito.when(commands.getAdditionalVars()).thenReturn(Collections.emptyMap());
Mockito.when(commands.getEnableOutputDirectory()).thenReturn(true);
Mockito.when(commands.outputDirectoryEnabled()).thenReturn(true);

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;

559
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.250",
"@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",
@@ -1968,23 +1968,6 @@
"mlly": "^1.7.4"
}
},
"node_modules/@iconify/utils/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@iconify/utils/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
@@ -1998,13 +1981,13 @@
}
},
"node_modules/@intlify/core-base": {
"version": "11.1.11",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.11.tgz",
"integrity": "sha512-1Z0N8jTfkcD2Luq9HNZt+GmjpFe4/4PpZF3AOzoO1u5PTtSuXZcfhwBatywbfE2ieB/B5QHIoOFmCXY2jqVKEQ==",
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.11",
"@intlify/shared": "11.1.11"
"@intlify/message-compiler": "11.1.12",
"@intlify/shared": "11.1.12"
},
"engines": {
"node": ">= 16"
@@ -2014,12 +1997,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.11",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.11.tgz",
"integrity": "sha512-7PC6neomoc/z7a8JRjPBbu0T2TzR2MQuY5kn2e049MP7+o32Ve7O8husylkA7K9fQRe4iNXZWTPnDJ6vZdtS1Q==",
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.11",
"@intlify/shared": "11.1.12",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -2030,9 +2013,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "11.1.11",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.11.tgz",
"integrity": "sha512-RIBFTIqxZSsxUqlcyoR7iiC632bq7kkOwYvZlvcVObHfrF4NhuKc4FKvu8iPCrEO+e3XsY7/UVpfgzg+M7ETzA==",
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -3219,28 +3202,28 @@
"license": "BSD-3-Clause"
},
"node_modules/@kestra-io/ui-libs": {
"version": "0.0.250",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.250.tgz",
"integrity": "sha512-Y0ANjGn91f+3G6ZeH0niorf0ZCNe/BPWfur+yHni4AKHbyNUZjrE8UN9ETvOlYe5c2qQSyQdM9yK/LdG1Thtzw==",
"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.16.1",
"@nuxtjs/mdc": "^0.17.3",
"@popperjs/core": "^2.11.8",
"html-to-image": "^1.11.11",
"mermaid": "^11.4.1",
"shiki": "^3.9.2",
"html-to-image": "^1.11.13",
"mermaid": "^11.11.0",
"shiki": "^3.12.2",
"slugify": "^1.6.6",
"vue-i18n": "^11.0.1"
"vue-i18n": "^11.1.12"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.24.2",
"@esbuild/darwin-x64": "^0.24.2",
"@esbuild/linux-x64": "^0.24.2",
"@rollup/rollup-darwin-arm64": "^4.30.1",
"@rollup/rollup-darwin-x64": "^4.30.1",
"@rollup/rollup-linux-x64-gnu": "^4.30.1",
"@swc/core-darwin-arm64": "^1.10.6",
"@swc/core-darwin-x64": "^1.10.6",
"@swc/core-linux-x64-gnu": "^1.10.6"
"@esbuild/darwin-arm64": "^0.25.9",
"@esbuild/darwin-x64": "^0.25.9",
"@esbuild/linux-x64": "^0.25.9",
"@rollup/rollup-darwin-arm64": "^4.50.1",
"@rollup/rollup-darwin-x64": "^4.50.1",
"@rollup/rollup-linux-x64-gnu": "^4.50.1",
"@swc/core-darwin-arm64": "^1.13.5",
"@swc/core-darwin-x64": "^1.13.5",
"@swc/core-linux-x64-gnu": "^1.13.5"
},
"peerDependencies": {
"@vue-flow/background": "^1.3.0",
@@ -3259,54 +3242,6 @@
"yaml": "^2.5.1"
}
},
"node_modules/@kestra-io/ui-libs/node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@kestra-io/ui-libs/node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@kestra-io/ui-libs/node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@kestra-io/ui-libs/node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -3571,32 +3506,32 @@
}
},
"node_modules/@nuxt/kit": {
"version": "3.18.1",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.18.1.tgz",
"integrity": "sha512-z6w1Fzv27CIKFlhct05rndkJSfoslplWH5fJ9dtusEvpYScLXp5cATWIbWkte9e9zFSmQTgDQJjNs3geQHE7og==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.1.3.tgz",
"integrity": "sha512-WK0yPIqcb3GQ8r4GutF6p/2fsyXnmmmkuwVLzN4YaJHrpA2tjEagjbxdjkWYeHW8o4XIKJ4micah4wPOVK49Mg==",
"license": "MIT",
"dependencies": {
"c12": "^3.2.0",
"c12": "^3.3.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.7",
"ignore": "^7.0.5",
"jiti": "^2.5.1",
"jiti": "^2.6.1",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"mlly": "^1.8.0",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.2.0",
"pkg-types": "^2.3.0",
"rc9": "^2.1.2",
"scule": "^1.3.0",
"semver": "^7.7.2",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.14",
"tinyglobby": "^0.2.15",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.2.0",
"unimport": "^5.4.1",
"untyped": "^2.0.0"
},
"engines": {
@@ -3604,20 +3539,23 @@
}
},
"node_modules/@nuxtjs/mdc": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@nuxtjs/mdc/-/mdc-0.16.1.tgz",
"integrity": "sha512-di9Ox9QY5pO2eIkQPyKFe9O8L3RvIrGbmjI3rJQRj1xGYRFj2S9RvBPCFbvfaqQGOTjOfxHLg8KtQIGj1Iw/lg==",
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@nuxtjs/mdc/-/mdc-0.17.4.tgz",
"integrity": "sha512-I5ZYUWVlE2xZAkfBG6B0/l2uddDZlr8X2WPVMPYNY4zocobBjMgykj4aqYXHY+N35HRYsa+IpuUCf30bR8xCbA==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.16.1",
"@shikijs/transformers": "^3.2.1",
"@nuxt/kit": "^4.1.1",
"@shikijs/core": "^3.12.2",
"@shikijs/langs": "^3.12.2",
"@shikijs/themes": "^3.12.2",
"@shikijs/transformers": "^3.12.2",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@vue/compiler-core": "^3.5.13",
"@vue/compiler-core": "^3.5.21",
"consola": "^3.4.2",
"debug": "4.4.0",
"debug": "^4.4.1",
"defu": "^6.1.4",
"destr": "^2.0.3",
"destr": "^2.0.5",
"detab": "^3.0.2",
"github-slugger": "^2.0.0",
"hast-util-format": "^1.1.0",
@@ -3625,32 +3563,56 @@
"hast-util-to-string": "^3.0.1",
"mdast-util-to-hast": "^13.2.0",
"micromark-util-sanitize-uri": "^2.0.1",
"parse5": "^7.2.1",
"parse5": "^8.0.0",
"pathe": "^2.0.3",
"property-information": "^7.0.0",
"property-information": "^7.1.0",
"rehype-external-links": "^3.0.0",
"rehype-minify-whitespace": "^6.0.2",
"rehype-raw": "^7.0.0",
"rehype-remark": "^10.0.0",
"rehype-remark": "^10.0.1",
"rehype-slug": "^6.0.0",
"rehype-sort-attribute-values": "^5.0.1",
"rehype-sort-attributes": "^5.0.1",
"remark-emoji": "^5.0.1",
"remark-emoji": "^5.0.2",
"remark-gfm": "^4.0.1",
"remark-mdc": "^3.5.3",
"remark-mdc": "v3.6.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
"scule": "^1.3.0",
"shiki": "^3.2.1",
"ufo": "^1.5.4",
"shiki": "^3.12.2",
"ufo": "^1.6.1",
"unified": "^11.0.5",
"unist-builder": "^4.0.0",
"unist-util-visit": "^5.0.0",
"unwasm": "^0.3.9",
"unwasm": "^0.3.11",
"vfile": "^6.0.3"
}
},
"node_modules/@nuxtjs/mdc/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/@nuxtjs/mdc/node_modules/parse5": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
@@ -4165,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"
@@ -4240,9 +4202,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz",
"integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==",
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
"cpu": [
"arm64"
],
@@ -4253,9 +4215,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz",
"integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==",
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
"cpu": [
"x64"
],
@@ -4420,9 +4382,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz",
"integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==",
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
"integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
"cpu": [
"x64"
],
@@ -4503,12 +4465,12 @@
"license": "Apache-2.0"
},
"node_modules/@shikijs/core": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.11.0.tgz",
"integrity": "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.13.0.tgz",
"integrity": "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.11.0",
"@shikijs/types": "3.13.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
@@ -4613,19 +4575,19 @@
}
},
"node_modules/@shikijs/transformers": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.11.0.tgz",
"integrity": "sha512-fhSpVoq0FoCtKbBpzE3mXcIbr0b7ozFDSSWiVjWrQy+wrOfaFfwxgJqh8kY3Pbv/i+4pcuMIVismLD2MfO62eQ==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.13.0.tgz",
"integrity": "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "3.11.0",
"@shikijs/types": "3.11.0"
"@shikijs/core": "3.13.0",
"@shikijs/types": "3.13.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.11.0.tgz",
"integrity": "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz",
"integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
@@ -6855,24 +6817,6 @@
}
}
},
"node_modules/@vitest/coverage-v8/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
@@ -7737,16 +7681,6 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
@@ -8151,22 +8085,22 @@
"license": "MIT"
},
"node_modules/c12": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.2.0.tgz",
"integrity": "sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.1.tgz",
"integrity": "sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
"defu": "^6.1.4",
"dotenv": "^17.2.1",
"dotenv": "^17.2.3",
"exsolve": "^1.0.7",
"giget": "^2.0.0",
"jiti": "^2.5.1",
"jiti": "^2.6.1",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.2.0",
"perfect-debounce": "^2.0.0",
"pkg-types": "^2.3.0",
"rc9": "^2.1.2"
},
"peerDependencies": {
@@ -8178,6 +8112,12 @@
}
}
},
"node_modules/c12/node_modules/perfect-debounce": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
"integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
"license": "MIT"
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -9559,9 +9499,9 @@
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -9912,9 +9852,9 @@
}
},
"node_modules/dotenv": {
"version": "17.2.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -11189,19 +11129,18 @@
}
},
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
"node": ">=12"
}
},
"node_modules/fs.realpath": {
@@ -14278,9 +14217,9 @@
}
},
"node_modules/jiti": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -14787,24 +14726,6 @@
"node": ">=20"
}
},
"node_modules/lint-staged/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/listr2": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.3.tgz",
@@ -15087,9 +15008,9 @@
}
},
"node_modules/magic-string": {
"version": "0.30.18",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -16235,15 +16156,15 @@
}
},
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"ufo": "^1.5.4"
"acorn": "^8.15.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.1"
}
},
"node_modules/mlly/node_modules/confbox": {
@@ -16956,15 +16877,15 @@
}
},
"node_modules/nypm": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
"pathe": "^2.0.3",
"pkg-types": "^2.2.0",
"pkg-types": "^2.3.0",
"tinyexec": "^1.0.1"
},
"bin": {
@@ -17112,16 +17033,6 @@
"node": ">=0.10.0"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -17298,9 +17209,9 @@
}
},
"node_modules/patch-package": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
"integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -17309,15 +17220,14 @@
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^9.0.0",
"fs-extra": "^10.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.0.33",
"tmp": "^0.2.4",
"yaml": "^2.2.2"
},
"bin": {
@@ -17328,20 +17238,6 @@
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/patch-package/node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
@@ -17613,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"
@@ -17632,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": {
@@ -18445,9 +18341,9 @@
}
},
"node_modules/remark-emoji": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.1.tgz",
"integrity": "sha512-QCqTSvcZ65Ym+P+VyBKd4JfJfh7icMl7cIOGVmPMzWkDtdD8pQ0nQG7yxGolVIiMzSx90EZ7SwNiVpYpfTxn7w==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.2.tgz",
"integrity": "sha512-IyIqGELcyK5AVdLFafoiNww+Eaw/F+rGrNSXoKucjo95uL267zrddgxGM83GN1wFIb68pyDuAsY3m5t2Cav1pQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.4",
@@ -19715,9 +19611,9 @@
}
},
"node_modules/strip-literal": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
@@ -19921,13 +19817,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
@@ -20016,16 +19912,13 @@
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
"node": ">=14.14"
}
},
"node_modules/tmpl": {
@@ -20421,9 +20314,9 @@
}
},
"node_modules/unctx/node_modules/unplugin": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.7.tgz",
"integrity": "sha512-zU7Osb4D5YNc9eLKsKaG6WQi9soLS+Yd9MDhOHlhAR+uoNy3BmWuddjLMhJpBpSBSIYtK5/MQvAWx9nAURTN6Q==",
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
@@ -20484,25 +20377,25 @@
}
},
"node_modules/unimport": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.2.0.tgz",
"integrity": "sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.5.0.tgz",
"integrity": "sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg==",
"license": "MIT",
"dependencies": {
"acorn": "^8.15.0",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"local-pkg": "^1.1.2",
"magic-string": "^0.30.19",
"mlly": "^1.8.0",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"pkg-types": "^2.2.0",
"pkg-types": "^2.3.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.14",
"unplugin": "^2.3.5",
"unplugin-utils": "^0.2.4"
"strip-literal": "^3.1.0",
"tinyglobby": "^0.2.15",
"unplugin": "^2.3.10",
"unplugin-utils": "^0.3.0"
},
"engines": {
"node": ">=18.12.0"
@@ -20533,9 +20426,9 @@
}
},
"node_modules/unimport/node_modules/unplugin": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.7.tgz",
"integrity": "sha512-zU7Osb4D5YNc9eLKsKaG6WQi9soLS+Yd9MDhOHlhAR+uoNy3BmWuddjLMhJpBpSBSIYtK5/MQvAWx9nAURTN6Q==",
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
@@ -20674,16 +20567,16 @@
}
},
"node_modules/unplugin-utils": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz",
"integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
"license": "MIT",
"dependencies": {
"pathe": "^2.0.3",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=18.12.0"
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
@@ -20750,9 +20643,9 @@
}
},
"node_modules/unwasm/node_modules/unplugin": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.7.tgz",
"integrity": "sha512-zU7Osb4D5YNc9eLKsKaG6WQi9soLS+Yd9MDhOHlhAR+uoNy3BmWuddjLMhJpBpSBSIYtK5/MQvAWx9nAURTN6Q==",
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
@@ -20890,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": {
@@ -20987,24 +20880,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-node/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -21109,24 +20984,6 @@
}
}
},
"node_modules/vitest/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
@@ -21431,13 +21288,13 @@
}
},
"node_modules/vue-i18n": {
"version": "11.1.11",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.11.tgz",
"integrity": "sha512-LvyteQoXeQiuILbzqv13LbyBna/TEv2Ha+4ZWK2AwGHUzZ8+IBaZS0TJkCgn5izSPLcgZwXy9yyTrewCb2u/MA==",
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.11",
"@intlify/shared": "11.1.11",
"@intlify/core-base": "11.1.12",
"@intlify/shared": "11.1.12",
"@vue/devtools-api": "^6.5.0"
},
"engines": {

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.250",
"@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

@@ -19,6 +19,7 @@
<script setup lang="ts">
import {computed, markRaw, onMounted, onUnmounted, ref, watch} from "vue";
import Utils from "../../utils/utils";
import {useStorage} from "@vueuse/core";
import {useI18n} from "vue-i18n";
import {useCoreStore} from "../../stores/core";
@@ -166,8 +167,10 @@
const TABS = isTourRunning.value ? DEFAULT_TOUR_TABS.flatMap(t => t.tabs) : DEFAULT_ACTIVE_TABS;
flowStore.creationId = flowStore.creationId ?? Utils.uid()
const panels = useStorage<Panel[]>(
`el-fl-${flowStore.flow?.namespace}-${flowStore.flow?.id}`,
`el-fl-${flowStore.flow?.namespace ?? `creation-${flowStore.creationId}`}${flowStore.flow?.id ? `-${flowStore.flow.id}` : ""}`,
TABS
.map((t) => ({
...staticGetPanelFromValue(t).panel,

View File

@@ -4,4 +4,7 @@
<script lang="ts" setup>
import Blueprints from "override/components/flows/blueprints/Blueprints.vue";
defineOptions({
inheritAttrs: false,
})
</script>

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

@@ -67,7 +67,10 @@ export function useBaseNamespacesStore() {
}
async function kv(this: any, payload: {namespace: string; key: string}) {
const response = await this.$http.get(`${apiUrl(this.vuexStore)}/namespaces/${payload.namespace}/kv/${payload.key}`);
const response = await this.$http.get(`${apiUrl(this.vuexStore)}/namespaces/${payload.namespace}/kv/${payload.key}`, VALIDATE);
if (response.status === 404) {
throw new Error(response.data.message);
}
const data = response.data;
const contentLength = response.headers?.["content-length"];
if (contentLength === (data.length + 2).toString()) {

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

@@ -2,6 +2,8 @@ import {defineStore} from "pinia";
import {apiUrl} from "override/utils/route";
import {trackBlueprintSelection} from "../utils/tabTracking";
export const VALIDATE = {validateStatus: (status: number) => status === 200 || status === 401};
interface Blueprint {
[key: string]: any;
}
@@ -66,7 +68,7 @@ export const useBlueprintsStore = defineStore("blueprints", {
const kind = options.kind && options.type !== "custom" ? `/${options.kind}` : "";
const response = await this.$http.get(
`${apiUrl(this.vuexStore)}/blueprints/${options.type}${kind}`,
{params: options.params}
{params: options.params, ...VALIDATE}
);
this.blueprints = response.data;
return response.data;
@@ -76,7 +78,7 @@ export const useBlueprintsStore = defineStore("blueprints", {
const kind = options.kind && options.type !== "custom" ? `/${options.kind}` : "";
const response = await this.$http.get(
`${apiUrl(this.vuexStore)}/blueprints/${options.type}${kind}/tags`,
{params: options.params}
{params: options.params, ...VALIDATE}
);
return response.data;
},

View File

@@ -91,6 +91,7 @@ export const useFlowStore = defineStore("flow", () => {
const haveChange = ref<boolean>(false)
const expandedSubflows = ref<string[]>([])
const metadata = ref<Record<string, any>>()
const creationId = ref<string>();
const store = useStore() as Store<any> & {
$http: {
@@ -490,8 +491,16 @@ export const useFlowStore = defineStore("flow", () => {
function createFlow(options: { flow: string }) {
return store.$http.post(`${apiUrl(store)}/flows`, options.flow, textYamlHeader).then(response => {
const creationPanels = localStorage.getItem(`el-fl-creation-${creationId.value}`) ?? YAML_UTILS.stringify([]);
localStorage.setItem(`el-fl-${flow.value!.namespace}-${flow.value!.id}`, creationPanels);
flow.value = response.data;
// clean-up
localStorage.removeItem(`el-fl-creation-${creationId.value}`);
creationId.value = undefined;
return response.data;
})
}
@@ -863,6 +872,7 @@ function deleteFlowAndDependencies() {
})
return {
creationId,
isFlow,
isAllowedEdit,
readOnlySystemLabel,

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

@@ -1,23 +1,22 @@
const maybeText = (allowSeparators: boolean) => "(?:\"[^\"]*\")|(?:'[^']*')|(?:(?:(?!\\}\\})" + (allowSeparators ? "[\\S\\n ]" : "[^~+,:\\n ]") + ")*)";
const maybeAnotherPebbleExpression = "(?:[\\n ]*\\{\\{[\\n ]*" + maybeText(true) + "[\\n ]*\\}\\}[\\n ]*)*";
const pebbleStart = "\\{\\{[\\n ]*";
const fieldWithoutDotCapture = "([^\\(\\)\\}:~+.\\n '\"]*)(?![^\\(\\)\\}\\n ])";
const dotAccessedFieldWithParentCapture = "([^\\(\\)\\}:~+\\n '\"]*)\\." + fieldWithoutDotCapture;
const maybeTextFollowedBySeparator = "(?:" + maybeText(false) + "[~+ ]+)*";
const paramKey = "[^\\n \\(\\)~+\\},:=]+";
const paramValue = "(?:(?:(?:\"[^\"]*\"?)|(?:'[^']*'?)|[^,)]))*";
const fieldWithoutDotCapture = "([^()}:~+.\\n '\"]*)(?![^()}\\n ])";
const dotAccessedFieldWithParentCapture = "([^()}:~+\\n '\"]*)\\." + fieldWithoutDotCapture;
const maybeTextFollowedBySeparator = "(?:" + maybeText(true) + "[\\n ]*(?:(?:[~+]+)|(?:\\}\\}[\\n ]*" + pebbleStart + "))[\\n ]*)*";
const paramKey = "[^\\n ()~+},:=]+";
const paramValue = "(?:(?:(?:\"[^\"]*\"?)|(?:'[^']*'?)|[^,)}]))*";
const maybeParams = "(" +
"(?:[\\n ]*" + paramKey + "[\\n ]*=[\\n ]*" + paramValue + "(?:[\\n ]*,[\\n ]*)?)+)?" +
"([^\\n \\(\\)~+\\},:=]*)?";
const functionWithMaybeParams = "([^\\n\\(\\)\\},:~ ]+)\\(" + maybeParams
"([^\\n ()~+},:=]*)?";
const functionWithMaybeParams = "([^\\n()},:~ ]+)\\(" + maybeParams
export default {
beforeSeparator: (additionalSeparators: string[] = []) => `([^\\}:\\n ${additionalSeparators.join("")}]*)`,
/** [fullMatch, dotForbiddenField] */
capturePebbleVarRoot: `${maybeAnotherPebbleExpression}${pebbleStart}${maybeTextFollowedBySeparator}${fieldWithoutDotCapture}`,
capturePebbleVarRoot: `${pebbleStart}${maybeTextFollowedBySeparator}${fieldWithoutDotCapture}`,
/** [fullMatch, parentFieldMaybeIncludingDots, childField] */
capturePebbleVarParent: `${maybeAnotherPebbleExpression}${pebbleStart}${maybeTextFollowedBySeparator}${dotAccessedFieldWithParentCapture}`,
capturePebbleVarParent: `${pebbleStart}${maybeTextFollowedBySeparator}${dotAccessedFieldWithParentCapture}`,
/** [fullMatch, functionName, textBetweenParenthesis, maybeTypedWordStart] */
capturePebbleFunction: `${maybeAnotherPebbleExpression}${pebbleStart}${maybeTextFollowedBySeparator}${functionWithMaybeParams}`,
capturePebbleFunction: `${pebbleStart}${maybeTextFollowedBySeparator}${functionWithMaybeParams}`,
captureStringValue: "^[\"']([^\"']+)[\"']$"
}

View File

@@ -109,16 +109,7 @@ tasks:
"\\")) | (.key + \\"->\\" + .value)"
}} {{myFunc(my-param_1='value1', my-param_2="value2", myK`
expect([...(regex.exec(shouldMatchLastFunction) ?? [])]).toEqual([
`id: breaking-ui
namespace: io.kestra.blx
description: "Upload multiple files to s3 sequentially"
tasks:
- id: placeholder
type: io.kestra.plugin.core.log.Log
message: |-
{{
`{{
"to_entries[] | select(.key | startswith(\\"" +
inputs.selector +
"\\")) | (.key + \\"->\\" + .value)"

View File

@@ -26,7 +26,9 @@ dependencies {
// ai
implementation("dev.langchain4j:langchain4j")
implementation("dev.langchain4j:langchain4j-google-ai-gemini:1.4.0")
implementation("dev.langchain4j:langchain4j-google-ai-gemini")
implementation("dev.langchain4j:langchain4j-http-client-jdk")
implementation('org.bouncycastle:bcpkix-jdk18on')
implementation("de.siegmar:fastcsv")

View File

@@ -1,2 +1,3 @@
# Keep the name static
micronaut.openapi.filename=kestra
micronaut.openapi.filename=kestra
micronaut.openapi.constructor-arguments-as-required=false

View File

@@ -44,12 +44,10 @@ import io.kestra.webserver.responses.BulkResponse;
import io.kestra.webserver.responses.PagedResults;
import io.kestra.webserver.services.ExecutionDependenciesStreamingService;
import io.kestra.webserver.services.ExecutionStreamingService;
import io.kestra.core.runners.SecureVariableRendererFactory;
import io.kestra.webserver.utils.PageableUtils;
import io.kestra.webserver.utils.RequestUtils;
import io.kestra.webserver.utils.filepreview.FileRender;
import io.kestra.webserver.utils.filepreview.FileRenderBuilder;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.core.annotation.Introspected;
@@ -73,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;
@@ -123,7 +124,7 @@ public class ExecutionController {
@Nullable
@Value("${micronaut.server.context-path}")
protected String basePath;
@Inject
private FlowRepositoryInterface flowRepository;
@@ -170,7 +171,7 @@ public class ExecutionController {
@Inject
private ApplicationEventPublisher<CrudEvent<Execution>> eventPublisher;
@Inject
private RunContextFactory runContextFactory;
@@ -188,7 +189,7 @@ public class ExecutionController {
@Inject
private Optional<OpenTelemetry> openTelemetry;
@Inject
private ExecutionStreamingService executionStreamingService;
@@ -208,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,
@@ -357,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()) {
@@ -378,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<>();
@@ -419,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,
@@ -668,8 +669,18 @@ 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
public Publisher<ExecutionResponse> createExecution(
@Parameter(description = "The flow namespace") @PathVariable String namespace,
@@ -997,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,
@@ -1057,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()) {
@@ -1240,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(
@@ -1303,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()
@@ -1513,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,
@@ -1622,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,
@@ -1666,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,
@@ -1710,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 {
@@ -1801,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
) {
@@ -2018,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
) {
@@ -2135,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(
@@ -2249,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,
@@ -2339,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

@@ -1,20 +1,13 @@
package io.kestra.webserver.controllers.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.kestra.core.docs.JsonSchemaGenerator;
import io.kestra.core.exceptions.FlowProcessingException;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.models.QueryFilter;
import io.kestra.core.models.HasSource;
import io.kestra.core.models.QueryFilter;
import io.kestra.core.models.SearchResult;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.FlowId;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.FlowScope;
import io.kestra.core.models.flows.FlowWithException;
import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.models.flows.GenericFlow;
import io.kestra.core.models.flows.*;
import io.kestra.core.models.hierarchies.FlowGraph;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.topologies.FlowTopology;
@@ -24,7 +17,6 @@ import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.models.validations.ModelValidator;
import io.kestra.core.models.validations.ValidateConstraintViolation;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.repositories.FlowTopologyRepositoryInterface;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.serializers.YamlParser;
import io.kestra.core.services.FlowService;
@@ -54,6 +46,8 @@ 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;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -229,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,
@@ -284,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(
@@ -341,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.
@@ -444,7 +439,8 @@ 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,
@Parameter(description = "The flow id") @PathVariable String id,
@@ -482,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(
@@ -624,13 +620,13 @@ public class FlowController {
@Post(uri = "/validate/task", consumes = MediaType.APPLICATION_JSON)
@Operation(tags = {"Flows"}, summary = "Validate task")
public ValidateConstraintViolation validateTask(
@RequestBody(description = "The task") @Body String task
@RequestBody(description = "The task") @Schema(implementation = Object.class) @Body String task
) {
ValidateConstraintViolation.ValidateConstraintViolationBuilder<?, ?> validateConstraintViolationBuilder = ValidateConstraintViolation.builder();
try {
var taskParse = parseTaskTrigger(task, Task.class);
modelValidator.validate(taskParse);
var parsedTask = parseTaskTrigger(task, Task.class);
modelValidator.validate(parsedTask);
} catch (ConstraintViolationException e) {
validateConstraintViolationBuilder.constraints(e.getMessage());
} catch (RuntimeException re) {
@@ -649,13 +645,13 @@ public class FlowController {
@Post(uri = "/validate/trigger", consumes = MediaType.APPLICATION_JSON)
@Operation(tags = {"Flows"}, summary = "Validate trigger")
public ValidateConstraintViolation validateTrigger(
@RequestBody(description = "The trigger") @Body String trigger
@RequestBody(description = "The trigger") @Schema(implementation = Object.class) @Body String trigger
) {
ValidateConstraintViolation.ValidateConstraintViolationBuilder<?, ?> validateConstraintViolationBuilder = ValidateConstraintViolation.builder();
try {
var triggerParse = parseTaskTrigger(trigger, AbstractTrigger.class);
modelValidator.validate(triggerParse);
var parsedTrigger = parseTaskTrigger(trigger, AbstractTrigger.class);
modelValidator.validate(parsedTrigger);
} catch (ConstraintViolationException e) {
validateConstraintViolationBuilder.constraints(e.getMessage());
} catch (RuntimeException re) {
@@ -672,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();
@@ -709,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);
@@ -747,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);
@@ -790,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);
@@ -822,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

@@ -13,6 +13,7 @@ import io.kestra.webserver.services.posthog.PosthogService;
import lombok.Getter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -72,7 +73,10 @@ public abstract class AiService<T extends AiConfiguration> implements AiServiceI
public String generateFlow(String ip, FlowGenerationPrompt flowGenerationPrompt) {
String parentSpanId = IdUtils.create();
Map<String, String> inputState = Map.of("flowYaml", flowGenerationPrompt.flowYaml(), "userPrompt", flowGenerationPrompt.userPrompt());
Map<String, String> inputState = new HashMap<>();
inputState.put("flowYaml", flowGenerationPrompt.flowYaml());
inputState.put("userPrompt", flowGenerationPrompt.userPrompt());
this.postHogService.capture(flowGenerationPrompt.conversationId(), "$ai_trace", Map.of(
"$ai_trace_id", flowGenerationPrompt.conversationId(),
"$ai_span_name", "FlowGenerationSession",

View File

@@ -1,7 +1,10 @@
package io.kestra.webserver.services.ai.gemini;
import dev.langchain4j.http.client.HttpClientBuilderLoader;
import dev.langchain4j.http.client.jdk.JdkHttpClientBuilder;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.googleai.GeminiThinkingConfig;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import io.kestra.core.docs.JsonSchemaGenerator;
import io.kestra.core.plugins.PluginRegistry;
@@ -9,10 +12,13 @@ import io.kestra.core.services.InstanceService;
import io.kestra.core.utils.VersionProvider;
import io.kestra.webserver.services.ai.AiService;
import io.kestra.webserver.services.posthog.PosthogService;
import io.kestra.webserver.utils.HttpClientUtils;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Singleton
@@ -27,7 +33,8 @@ public class GeminiAiService extends AiService<GeminiConfiguration> {
}
public ChatModel chatModel(List<ChatModelListener> listeners) {
return GoogleAiGeminiChatModel.builder()
GoogleAiGeminiChatModel.GoogleAiGeminiChatModelBuilder builder = GoogleAiGeminiChatModel.builder()
.baseUrl(getAiConfiguration().baseUrl())
.listeners(listeners)
.modelName(getAiConfiguration().modelName())
.apiKey(getAiConfiguration().apiKey())
@@ -37,7 +44,22 @@ public class GeminiAiService extends AiService<GeminiConfiguration> {
.maxOutputTokens(getAiConfiguration().maxOutputTokens())
.logRequests(getAiConfiguration().logRequests())
.logResponses(getAiConfiguration().logResponses())
.build();
.thinkingConfig(GeminiThinkingConfig.builder().includeThoughts(false).build())
.returnThinking(false);
if (getAiConfiguration().clientPem() != null) {
try (ByteArrayInputStream is = new ByteArrayInputStream(getAiConfiguration().clientPem().getBytes(StandardCharsets.UTF_8));
ByteArrayInputStream caPem = getAiConfiguration().caPem() == null ? null : new ByteArrayInputStream(getAiConfiguration().caPem().getBytes(StandardCharsets.UTF_8))) {
JdkHttpClientBuilder jdkHttpClientBuilder = ((JdkHttpClientBuilder) HttpClientBuilderLoader.loadHttpClientBuilder()).httpClientBuilder(
HttpClientUtils.withPemCertificate(is, caPem)
);
builder = builder.httpClientBuilder(jdkHttpClientBuilder);
} catch (Exception e) {
throw new IllegalArgumentException("Exception while trying to setup AI Service certificates", e);
}
}
return builder.build();
}
}

View File

@@ -4,9 +4,12 @@ import io.kestra.webserver.services.ai.AiConfiguration;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.annotation.Bindable;
import io.swagger.v3.oas.annotations.media.Schema;
@ConfigurationProperties(value = "kestra.ai.gemini")
public record GeminiConfiguration (
@Nullable
String baseUrl,
String apiKey,
@Bindable(defaultValue = "gemini-2.5-flash")
String modelName,
@@ -16,6 +19,11 @@ public record GeminiConfiguration (
Double topP,
@Nullable
Integer topK,
@Nullable
String clientPem,
@Schema(description = "Not required but can be useful to add further trust", nullable = true)
@Nullable
String caPem,
@Bindable(defaultValue = "8000")
int maxOutputTokens,
@Bindable(defaultValue = "false")

View File

@@ -0,0 +1,63 @@
package io.kestra.webserver.utils;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.http.HttpClient;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
public class HttpClientUtils {
public static HttpClient.Builder withPemCertificate(InputStream clientPemIs, InputStream caPem) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
PrivateKey privateKey = null;
Certificate clientCertificate = null;
// Parse the PEM content to extract certificate and private key
try (PEMParser pemParser = new PEMParser(new InputStreamReader(clientPemIs))) {
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter();
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter();
Object object;
while ((object = pemParser.readObject()) != null) {
if (object instanceof PrivateKeyInfo privateKeyInfo) {
privateKey = keyConverter.getPrivateKey(privateKeyInfo);
} else if (object instanceof X509CertificateHolder) {
clientCertificate = certConverter.getCertificate((X509CertificateHolder) object);
}
}
}
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null, null);
Certificate[] privateKeyCertificatesChain = new Certificate[]{clientCertificate};
if (caPem != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
keyStore.setCertificateEntry("ca", cf.generateCertificate(caPem));
}
keyStore.setKeyEntry("client-key", privateKey, "".toCharArray(), privateKeyCertificatesChain);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return HttpClient.newBuilder().sslContext(sslContext);
}
}

View File

@@ -0,0 +1,127 @@
package io.kestra.webserver.controllers.api;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import com.github.tomakehurst.wiremock.admin.model.GetServeEventsResult;
import com.github.tomakehurst.wiremock.http.Body;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import io.kestra.core.utils.IdUtils;
import io.kestra.webserver.models.ai.FlowGenerationPrompt;
import io.kestra.webserver.utils.PosthogUtil;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.kestra.core.junit.annotations.KestraTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.util.Objects;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest
@WireMockTest(httpPort = 28181)
class AiControllerTest {
@Inject
@Client("/")
HttpClient client;
@RegisterExtension
static WireMockExtension extension = WireMockExtension.newInstance()
.options(wireMockConfig()
.dynamicPort()
.httpsPort(28183)
.keystorePath(Objects.requireNonNull(AiControllerTest.class.getClassLoader().getResource("mtls/server-keystore.p12")).getPath())
.keystorePassword("keystorePassword")
.keyManagerPassword("keystorePassword")
.keystoreType("PKCS12")
.needClientAuth(true) // This enables mTLS
.trustStorePath(Objects.requireNonNull(AiControllerTest.class.getClassLoader().getResource("mtls/client-truststore.p12")).getPath()) // Contains trusted client CAs
.trustStorePassword("changeit")
.trustStoreType("PKCS12"))
.build();
@BeforeEach
void baseMocks(WireMockRuntimeInfo wmRuntimeInfo) {
PosthogUtil.mockPosthog(wmRuntimeInfo);
}
@Test
void mTLS() {
extension.stubFor(post(anyUrl())
.inScenario("Regular flow generation")
.whenScenarioStateIs("Started")
.willReturn(
aResponse().withResponseBody(
Body.fromJsonBytes("""
{
"responseId" : "3NvjaPPRAo_WvdIP46DvmQE",
"modelVersion" : "gemini-2.5-flash",
"candidates" : [ {
"content" : {
"parts" : [ {
"text" : "io.kestra.plugin.core.log.Log"
} ],
"role" : "model"
},
"finishReason" : "STOP",
"index" : 0
} ],
"usageMetadata" : {
"promptTokenCount" : 3658,
"candidatesTokenCount" : 25,
"totalTokenCount" : 3939
}
}""".getBytes()
)))
.willSetStateTo("Tasks fetched"));
String expectedFlowResponse = "id: my-flow\\nnamespace: io.kestra.tests\\ntasks:\\n - id: log\\n type: io.kestra.plugin.core.log.Log\\n format: \\\"hi\\\"";
extension.stubFor(post(anyUrl())
.inScenario("Regular flow generation")
.whenScenarioStateIs("Tasks fetched")
.willReturn(
aResponse().withResponseBody(
Body.fromJsonBytes("""
{
"responseId" : "3NvjaPPRAo_WvdIP46DvmQF",
"modelVersion" : "gemini-2.5-flash",
"candidates" : [ {
"content" : {
"parts" : [ {
"text" : "%s"
} ],
"role" : "model"
},
"finishReason" : "STOP",
"index" : 0
} ],
"usageMetadata" : {
"promptTokenCount" : 3658,
"candidatesTokenCount" : 25,
"totalTokenCount" : 3939
}
}""".formatted(expectedFlowResponse).getBytes()
))));
HttpResponse<String> response = client.toBlocking().exchange(
HttpRequest.POST("/api/v1/main/ai/generate/flow", new FlowGenerationPrompt(IdUtils.create(), "Say 'hi'", null)),
String.class
);
GetServeEventsResult serveEvents = extension.getServeEvents();
serveEvents.getServeEvents().forEach(serveEvent -> {
assertThat(serveEvent.getResponse().getStatus()).isEqualTo(200);
});
assertThat(response.getStatus().getCode()).isEqualTo(200);
assertThat(response.getBody().get()).isEqualTo(expectedFlowResponse.replace("\\n", "\n").replace("\\\"", "\""));
}
}

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);

View File

@@ -54,7 +54,7 @@ class MiscControllerTest {
assertThat(response.getUuid()).isNotNull();
assertThat(response.getIsTaskRunEnabled()).isFalse();
assertThat(response.getIsAnonymousUsageEnabled()).isTrue();
assertThat(response.getIsAiEnabled()).isFalse();
assertThat(response.getIsAiEnabled()).isTrue();
assertThat(response.getSystemNamespace()).isEqualTo("some.system.ns");
}

View File

@@ -0,0 +1,28 @@
package io.kestra.webserver.utils;
import com.github.tomakehurst.wiremock.http.Body;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import java.nio.charset.StandardCharsets;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
public class PosthogUtil {
public static void mockPosthog(WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(get(urlEqualTo("/v1/config"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withResponseBody(Body.fromJsonBytes(
"""
{
"posthog": {
"apiHost": "%s"
}
}""".formatted(wmRuntimeInfo.getHttpBaseUrl()).getBytes(StandardCharsets.UTF_8)
))));
stubFor(post(urlEqualTo("/batch"))
.willReturn(aResponse()));
}
}

View File

@@ -30,6 +30,123 @@ jackson:
FAIL_ON_UNKNOWN_PROPERTIES: false
kestra:
ai:
type: gemini
gemini:
base-url: https://localhost:28183
api-key: "fakeApiKey"
ca-pem: |-
-----BEGIN CERTIFICATE-----
MIIFCTCCAvGgAwIBAgIUQY9OI1ErtFFu3iujvLu5NzPqBrowDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTAwNjE1NTc0OFoXDTM1MTAw
NDE1NTc0OFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAn8x9Byga8F9vBQtZTUBvAzDMLF4MGI+L7TJXAMzziAPo
uagMq1b7V+Ni3ZBBp84fuxfgowkJ9t2B5rvUQgf+Tc6x2N/5diVyThi/GCRt1nCl
LXATsetQqYHaoJmjBBvbRdOrtAUwvfDXWO2PZDLOrAFa29EurRTcBJ1uv/Uegcd9
sTb9yzAvNCppLidaGwT7JVcHiYcDekEz/3jCFQKwvBWNOFgX3JvwF8+5pHVdKRgE
/IqpgYErnJjcTWCSK0M/HkM8eUHxtztCSUkO5ak4NM521IxGA2jOQfgTVONbSjW1
MJ4CWsdTxcHcX9TwIRbeNxBI9muRIdLcHR4l4w69QxQJtY1jGmW8fClfAoVO8Obz
4wp7VuzkE9oTDtR95iVKO+9M15mSofkg3SFHE2O0aTIDGneEaHKH6NTIiPcLR/xW
QV1ZFHSM7cbh0iZh5kQFphZ6UGjqXWN3xm77er/4i7QoLY4342eicqHBSS+ovoyq
RdUYpZgyTKA5bIxiuVwcGBjyZfFpoxL50efcTLE8M01BOwHuoYtb9gARAGVotgmI
n6O7GzrTj1z43dPH1+u2pshzz67W0+vzBz/lNXgpa3Wxwg4kFCRnw34mEu+cd+tQ
tzSNtZm9wc0YYLT21xlFIacE2fmsWPUC28OSiLdDj3lKbJnDOogjPtEsMnTl4LsC
AwEAAaNTMFEwHQYDVR0OBBYEFONyqmci0bm+Nu8SqHwz0FGBBgsQMB8GA1UdIwQY
MBaAFONyqmci0bm+Nu8SqHwz0FGBBgsQMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBAGJGpb1vQtvBQCiC58F0jAlIoSCleifKbdlQOMcanMpr/zaG
U4L2MLZGVuWpmfEW1VPyxzbECsU2tHcWno4iJ7dj7PDPEBMKwWTa55SNXIU227Ef
lYMfnXYwgg/9Q6K/U8uXvhB3hquDE03YV2xrOhWP+fROCAgTnOMI3XvYcBpTMhFb
YZifc4HUWNM0+M6cSWV4D4NgI/piY5dvcGwv5U/WrlaYAD+HBJ9/xyaLcirIhRm4
qZUBfhwA2zuzVoC45iA3hm7Go2eK5UCxHGXzi3CAi2fD8/ibhi05ExkDKkePP4BI
WOCgOJ2zGv9KqE8Bq962ipshGl6ceIdOE0uRLhLWVWAA/lgtBkP1871WlXjcdf0t
9GZ2ORMwlLZo7SvQ4EKXrfmrg24nW5Bf+R6vd67nWiGopnUdgE8cdzEP1U0YnXTq
bs6kr2RpdNw/0EyDOU7/lJYvZUh2/ImSEe014B+WwLy7VsFHzqwz/aaivlcwpsrt
BWY7NfBWEPQnofbE4MOH0G6ueFcYUbBWP3EG2BN0hz7rTueQPemCb6NrWR09XPs9
AMVo3oupFL0WR6acXV87fCukNTVLEEn5UHKFSOgoPD+vy1dHXov1Ej2VwGP94Dec
mRdgSSfyRhyPGwlxjaiPcTLIkaJBEdR3H5Xzws87QT+SlKiCOrFjksPvYWXs
-----END CERTIFICATE-----
client-pem: |-
-----BEGIN CERTIFICATE-----
MIIE+DCCAuCgAwIBAgIUbbuIICr5CKGTNxe2P9a0D2xuwx4wDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTAwNjE1NTgwNFoXDTMwMDQx
MzE1NTgwNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAtuVdYOFn5rXr4EocBKuvCsPLXAWJzgioUlDZ9LDjdS+7
L3i+cW/3Msig7ihv/85cYjNeVVnB3JslgcpcE5KMSkdjZiEvd7LeSbfWYINTTX3m
xQFg2XZm6xH6bCX9yhe32jtr/Ib9eS+Z+YpoPzeqGuC8gXHwZi621Al17NdXEEJD
qmVtRsPFUE0d9/E4uPlxJ92jqV8XyG6QColQRWGUpmLWjF3eVtdBGmqr454PDI6x
brKpoIWF5qwcjHzEg7QcWJrAS/nRf3S0SYxEChEodASgqsyI1sCi2qbC98Mjk0UE
gqaxvcQNmIlGyxHHugr1UGc1FbziRVxwJnLS9ybGDOj1LZPXbW1kkz5+vSvByWNf
nISk9FKtdS7ehsHh1Sv55kGijC4f7cE2nf+WGXWv2j9gyBjhU3LIE0efjXHRBqpc
9Bw+8DvXK/JXM1uexTLlxlC9fy+vuo8DJYaThQTJIFRwY/TDNdcoQze+O+wjlNe0
TPjMQScGODGMKvduDrnGeht7qi2mFnczdskfYVTTCJ5YX/rXQFdK6XlGSK3ZQZeJ
4kZm2UZrtHE2z4RununXONmqCLT63aVrdZI8drs+zKH5xFvHfZgonMuV5JVJsZSt
JpM2K2sRk9W9h5OegsEpymVJLxAbXD3KNUa7sWv/NRnfFfRyGJjuGesbV6bTCM0C
AwEAAaNCMEAwHQYDVR0OBBYEFMBvh+ehi6bnz+E7UgX64J9CEEYWMB8GA1UdIwQY
MBaAFONyqmci0bm+Nu8SqHwz0FGBBgsQMA0GCSqGSIb3DQEBCwUAA4ICAQB6jHm1
/QUF22ThKuFSsw8scPKdKmDaxdoZkQ5JMUOAa1T9FzUpjYCn0yUFe/YmpwrF0ubA
+SOQZIfijC5iRsftexavNCmc1B/Z91uaplF9PocI/MkuDPIa3vFQioPw3z0H6ue4
m5mr0S7tUwt9dKJdPIGy5Zw2oq1JXpq04XL+fvigoNCh3i09xIraBEuzA1ra9PKM
IJXuj340MuNF+HkvEwlKRkSeQpNDinaIphzoUPXP/oeo6ite3lWuurz8kgrTNFAz
Em82pFaYH5RGWDaVJb1z/plhfAC4AZ4xylb3tbIAamCjJoWzGwFq3odKhIS1YkBv
yBJsW2f52dEOzs6l0xVZy+f8GfwFS3UAOjzeAWOaw1BJ8Zk8Tt5gQ0DOKV2Wum8x
PO3K4ITs7Ep/QNHHk7QtaTOBNPK8iA0RRkn63p/3TlA2E716KyCPmkMEvrRojocz
eJvUGW/TJmRkQVp5cI1ALDuuQJgxBD7k6JnIuzMALOaySLqCkwCZz/Idb7ojX219
K8g24o/vP83SgfTWd1FIon8wBnfgV1nGOQeTB9Rp5+ve4fOmVAcctY2bZNVRmA2h
ExqHQaOTUCm3W04sWQPZRTdf14LQvlP5qvUdgX04bmLPphDqABFuYkPRoTcutGm2
UtYRdDVx60ei9XePaJbsNJctiDPvQJX9nhZjcQ==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC25V1g4Wfmtevg
ShwEq68Kw8tcBYnOCKhSUNn0sON1L7sveL5xb/cyyKDuKG//zlxiM15VWcHcmyWB
ylwTkoxKR2NmIS93st5Jt9Zgg1NNfebFAWDZdmbrEfpsJf3KF7faO2v8hv15L5n5
img/N6oa4LyBcfBmLrbUCXXs11cQQkOqZW1Gw8VQTR338Ti4+XEn3aOpXxfIbpAK
iVBFYZSmYtaMXd5W10Eaaqvjng8MjrFusqmghYXmrByMfMSDtBxYmsBL+dF/dLRJ
jEQKESh0BKCqzIjWwKLapsL3wyOTRQSCprG9xA2YiUbLEce6CvVQZzUVvOJFXHAm
ctL3JsYM6PUtk9dtbWSTPn69K8HJY1+chKT0Uq11Lt6GweHVK/nmQaKMLh/twTad
/5YZda/aP2DIGOFTcsgTR5+NcdEGqlz0HD7wO9cr8lczW57FMuXGUL1/L6+6jwMl
hpOFBMkgVHBj9MM11yhDN7477COU17RM+MxBJwY4MYwq924OucZ6G3uqLaYWdzN2
yR9hVNMInlhf+tdAV0rpeUZIrdlBl4niRmbZRmu0cTbPhG6e6dc42aoItPrdpWt1
kjx2uz7MofnEW8d9mCicy5XklUmxlK0mkzYraxGT1b2Hk56CwSnKZUkvEBtcPco1
Rruxa/81Gd8V9HIYmO4Z6xtXptMIzQIDAQABAoICAAzt0Y2NMZ6bYqAZgmSJdxdw
OC/PoRUPA0hvuGX18TEHXZihVPkePJ/QPfbH+1mJJCUO/jlTXdhzS+dNFPg55lqZ
NFDJF3AYlWJ/BpO5BN+tVEMTR149YqYgaw76b+KO4wDK2Ds4QbKE58ITri3giKZb
bQf2NCkiXpXw+0ScWRETAMc1YfsG0NZ4inNMupac5N+4E8nf09sShx0vkByaJXt4
8N4TA/995Pdx7f0VCc74nANiyYn0QRR0Oz+/0YEr73W2BCelcnwA6ykTK3t4s17h
it1iwGj0oi6JwQOUaFnl9EfWHSojMgDZR3IIgObJFTFeB7XQg0BQT19Fw7MUAHD/
mCVg/s37QLhbb6BhIWUlLdEhV+Bqu5sa+GyCQyNpDPLyYTyIXcqYGBuqt6ZTdeqx
4rOqUQRW5tUhklHT3BHjGDehYTlT4VVpfce9Yji0BiHqieJgY4ohJa6EbpR80GSI
ynADH7VVF84XNONlhzCUcJVvHhKNUjY7cHGK2thzIyl+MXb/lBYFt63/sg2mwnYH
1DqE+G+NaZbPS5JPmNAvP+AjmDcuq32ImSK0RPJM66hdBGTcrjYzH5q496Vsbdrr
6Y7scRzVuRx7tmSjfRQMSfWuK9MYqTZxHGdVjN27kyaG6+8OYfOEfU6avT4pSWzV
hC/cGeVDsqIO0pQtJlqBAoIBAQDbxLsKXTqOz7HYISf8csFEEHuLahQsL8PEK++p
SPhiVZH88bqochaMi4nqe8BEa4zbl0YGKrXL4VTzS8Jyg2f91jPoOt9m94gQ53Oj
AQjPzYgXpEkctJsRSGciIoawNvJzUi8eDfoSa4LtrM6zR3n+A2uxNmw7p1N+6dhG
dcicKibxSkhmKlhfGdSJBqCptBwYIQ61eDS3CaA7j5WYZ3tA46o/XlrREUUIy5ke
Sh11MuiQcuB5N7VvHJngSewY4htLIcStOSk9JqOlIuLiewo8C6BdZN6zrdU/vY4M
svdH5/iKdTo2a7AAuV8GffuoMZw3+xb4t90prIe7u7+6qIWNAoIBAQDVDHBypU9s
4wSglJx7qOcHLDnEJ47ODLfgsu6fkLYro95sFFCb/pfEViRdkLCROhbl7MTZ4T9u
yWsPrnVF+8mxNVNEL9PgYHBmNdXQsTf51nZFyC4vUHxsoztOMdtSOqsjeZOoWliY
rhkBSRozspdUaM2f1z0DMydlD5ZIXSYFZHFfBYrxoZRPzEnhtqtZan3FjgZ39lSo
UvTQSEB4SNTlOrrxqA3LTm7ZtM7vIKI/zvwLivP5UewQYA1GmRqKKYXpB34e7goB
jGHtu+ZckyE+MusYLkPpqpRJYmROVZP6N3144r+S1g4J8ZCmtz7puTy77WfpPSUt
0XUTbIFiFaBBAoIBAQDPuErxqNzITxdRqUUaH3z80Hd1dnZKrXrj2INWBlp+11J9
Oh2rSOp3PQzGTOGVyfIBPCI7gfMDGaAptdm8UuffzK6TOdIeiKhbEekCkN+7ShDw
B5/zOeG3nC+e2/Niaw0OYweV6LAM6QF/lG5qlYyAwsrvXPlACQ+qTWzWbE2JDW5x
cjysFCoi+U8hlNoWjN5hEB3O+CcbOkXxBe3ndyfQVV99NbmxEhmmDopTTso5FD0t
CueQq08aDnaCwFwfyNbzVJ+I+xY3bmYOl8LLPnCWAIc6vzSfsBZ3gLra1e1UUbVh
aRv0hCR/Crb+c+WBPCLj5rf3rhGkaiaBExxNWSuhAoIBAHphUN6qUvuLVIchltn5
5eva3bvttTxrVdy/LA+AwwRCd2vCJ6PUFT309aLBkIt7wNeGsHUvzI5JKTSy1C7F
OdCPfys2BhFnlGlCF3ZvtmXPadUf7lfDdhW5lkGOphuQE+qm1cjpTOc3aqmwYlq8
Be243hUpQKTr539H+t3KlCKY1f7tYij21gkYooADvF36rBClStXyLCMctABI4K+3
toOOvwA9tt7ISSjJke0O4+Sfc2z1/ruC8YVeh4G8ROCEhaWcZjhCKIFHMKGtQ+B9
q6Tc/uq++Mfq8o9M862DvyMGaQ7dwYoJZ7sShjMeJAeCHn8dGomCVC8DmKr1s0Sy
g4ECggEAVd9jSlvn9374COKieU4sxO01Ms7/ICyyIAe1lXZCp7BdPzbxnKB2REgl
nWbpZ3nxogp7vsk3wBYo38uvXjR/ReE1yp6jUcBZgcGqh1hHWwTNLVkTrOrg5UpV
6LJ9v3REI5N0kNQM+4OubhrS62dDCpzAON6LRyHoi3zFdTTgTm0XFx7ttybx2Xn1
Ze1FgzhMERJaBS/xhQfVFczUZ2Zmv5D3SDhlDIZLXQEc+qHn9Qm5pfVdxnqcAqA8
gjEw0qur7SE6U7WN/CrsvqMDIdiBjoNh9yyVW9b+eJJ8qH4L+AJ2B6LoxOAlE+7O
NGLwUUB3wlL2w0B6cBCFQ2J3TtRVBQ==
-----END PRIVATE KEY-----
url: http://localhost:8081
encryption:
secret-key: I6EGNzRESu3X3pKZidrqCGOHQFUFC0yK
@@ -77,4 +194,4 @@ flyway:
locations:
- classpath:migrations/h2
ignore-migration-patterns: "*:missing,*:future"
out-of-order: true
out-of-order: true

Binary file not shown.