Compare commits

...

65 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
github-actions[bot]
41468652d4 chore(version): update to version '1.0.5' 2025-10-09 14:03:47 +00:00
Loïc Mathieu
bc182277de fix(system): refactor concurrency limit to use a counter
A counter allow to lock by flow which solves the race when two executions are created at the same time and the executoion_runnings table is empty.

Evaluating concurrency limit on the main executionQueue method also avoid an unexpected behavior where the CREATED execution is processed twice as its status didn't change immediatly when QUEUED.

Closes https://github.com/kestra-io/kestra-ee/issues/4877
2025-10-09 15:40:44 +02:00
Roman Acevedo
8c2271089c test: re enabling shouldGetReport, unflaky it with fixed date 2025-10-08 13:21:06 +02:00
Sanket Mundra
9973a2120b fix(backend): failing /resume/validate endpoint for integer label values (#11688)
* fix: cast label values to string

* fix: use findByIdWithSourceWithoutAcl() instead of findByIdWithoutAcl() and add test

* remove unwanted files
2025-10-08 10:13:31 +02:00
Roman Acevedo
bdfd038d40 ci: change Dockerfile.pr to dynamic version 2025-10-07 19:03:09 +02:00
YannC
a3fd734082 fix: modify annotations to improve openapi spec file generated (#11785) 2025-10-07 16:41:45 +02:00
130 changed files with 2347 additions and 1151 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

@@ -1,4 +1,5 @@
FROM kestra/kestra:develop
ARG KESTRA_DOCKER_BASE_VERSION=develop
FROM kestra/kestra:$KESTRA_DOCKER_BASE_VERSION
USER root

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

@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.State;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.runners.ExecutionQueued;
import io.kestra.core.services.ConcurrencyLimitService;
import io.kestra.jdbc.runner.AbstractJdbcExecutionQueuedStorage;
import io.micronaut.context.ApplicationContext;
import jakarta.inject.Inject;
@@ -15,8 +16,6 @@ import picocli.CommandLine;
import java.util.Optional;
import static io.kestra.core.utils.Rethrow.throwConsumer;
@CommandLine.Command(
name = "submit-queued-execution",
description = {"Submit all queued execution to the executor",
@@ -49,9 +48,11 @@ public class SubmitQueuedCommand extends AbstractCommand {
}
else if (queueType.get().equals("postgres") || queueType.get().equals("mysql") || queueType.get().equals("h2")) {
var executionQueuedStorage = applicationContext.getBean(AbstractJdbcExecutionQueuedStorage.class);
var concurrencyLimitService = applicationContext.getBean(ConcurrencyLimitService.class);
for (ExecutionQueued queued : executionQueuedStorage.getAllForAllTenants()) {
executionQueuedStorage.pop(queued.getTenantId(), queued.getNamespace(), queued.getFlowId(), throwConsumer(execution -> executionQueue.emit(execution.withState(State.Type.CREATED))));
Execution restart = concurrencyLimitService.unqueue(queued.getExecution(), State.Type.RUNNING);
executionQueue.emit(restart);
cpt++;
}
}

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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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 = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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 = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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 = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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

@@ -61,18 +61,22 @@ public abstract class AbstractFlow implements FlowInterface {
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
@Schema(
description = "Labels as a list of Label (key/value pairs) or as a map of string to string.",
oneOf = {
Label[].class,
Map.class
}
description = "Labels as a list of Label (key/value pairs) or as a map of string to string.",
oneOf = {
Label[].class,
Map.class
}
)
@Valid
List<Label> labels;
@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE)
@Schema(
type = "object",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
)
Map<String, Object> variables;
@Valid
private WorkerGroup workerGroup;

View File

@@ -61,6 +61,11 @@ public class Flow extends AbstractFlow implements HasUID {
}
});
@Schema(
type = "object",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
)
Map<String, Object> variables;
@Valid

View File

@@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.kestra.core.models.flows.input.*;
import io.kestra.core.models.property.Property;
import io.kestra.core.runners.RunContext;
import io.micronaut.core.annotation.Introspected;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.ConstraintViolationException;
@@ -18,8 +17,6 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.function.Function;
@SuppressWarnings("deprecation")
@SuperBuilder
@Getter

View File

@@ -1,6 +1,7 @@
package io.kestra.core.models.flows;
import io.micronaut.core.annotation.Introspected;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -33,6 +34,12 @@ public class Output implements Data {
* The output value. Can be a dynamic expression.
*/
@NotNull
@Schema(
oneOf = {
Object.class,
String.class
}
)
Object value;
/**

View File

@@ -2,6 +2,7 @@ package io.kestra.core.models.flows;
import io.kestra.core.validations.PluginDefaultValidation;
import io.micronaut.core.annotation.Introspected;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -21,6 +22,10 @@ public class PluginDefault {
@Builder.Default
private final boolean forced = false;
@Schema(
type = "object",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
)
private final Map<String, Object> values;
}

View File

@@ -12,6 +12,7 @@ import com.google.common.annotations.VisibleForTesting;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.runners.RunContext;
import io.kestra.core.serializers.JacksonMapper;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -36,6 +37,12 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Schema(
oneOf = {
Object.class,
String.class
}
)
public class Property<T> {
// By default, durations are stored as numbers.
// We cannot change that globally, as in JDBC/Elastic 'execution.state.duration' must be a number to be able to aggregate them.
@@ -68,7 +75,7 @@ public class Property<T> {
String getExpression() {
return expression;
}
/**
* Returns a new {@link Property} with no cached rendered value,
* so that the next render will evaluate its original Pebble expression.
@@ -84,9 +91,9 @@ public class Property<T> {
/**
* Build a new Property object with a value already set.<br>
*
* <p>
* A property build with this method will always return the value passed at build time, no rendering will be done.
*
* <p>
* Use {@link #ofExpression(String)} to build a property with a Pebble expression instead.
*/
public static <V> Property<V> ofValue(V value) {
@@ -126,12 +133,12 @@ public class Property<T> {
/**
* Build a new Property object with a Pebble expression.<br>
*
* <p>
* Use {@link #ofValue(Object)} to build a property with a value instead.
*/
public static <V> Property<V> ofExpression(@NotNull String expression) {
Objects.requireNonNull(expression, "'expression' is required");
if(!expression.contains("{")) {
if (!expression.contains("{")) {
throw new IllegalArgumentException("'expression' must be a valid Pebble expression");
}
@@ -140,7 +147,7 @@ public class Property<T> {
/**
* Render a property then convert it to its target type.<br>
*
* <p>
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
*
* @see io.kestra.core.runners.RunContextProperty#as(Class)
@@ -151,14 +158,14 @@ public class Property<T> {
/**
* Render a property with additional variables, then convert it to its target type.<br>
*
* <p>
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
*
* @see io.kestra.core.runners.RunContextProperty#as(Class, Map)
*/
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
if (property.value == null) {
String rendered = context.render(property.expression, variables);
String rendered = context.render(property.expression, variables);
property.value = MAPPER.convertValue(rendered, clazz);
}
@@ -167,7 +174,7 @@ public class Property<T> {
/**
* Render a property then convert it as a list of target type.<br>
*
* <p>
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
*
* @see io.kestra.core.runners.RunContextProperty#asList(Class)
@@ -178,7 +185,7 @@ public class Property<T> {
/**
* Render a property with additional variables, then convert it as a list of target type.<br>
*
* <p>
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
*
* @see io.kestra.core.runners.RunContextProperty#asList(Class, Map)
@@ -218,25 +225,25 @@ public class Property<T> {
/**
* Render a property then convert it as a map of target types.<br>
*
* <p>
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
*
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class)
*/
public static <T, K,V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
return asMap(property, runContext, keyClass, valueClass, Map.of());
}
/**
* Render a property with additional variables, then convert it as a map of target types.<br>
*
* <p>
* This method is safe to be used as many times as you want as the rendering and conversion will be cached.
* Warning, due to the caching mechanism, this method is not thread-safe.
*
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class, Map)
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public static <T, K,V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
if (property.value == null) {
JavaType targetMapType = MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass);

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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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

@@ -7,7 +7,6 @@ import io.kestra.core.models.Plugin;
import io.kestra.core.models.PluginVersioning;
import io.kestra.core.models.WorkerJobLifecycle;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.property.Property;
import io.kestra.core.runners.RunContext;
import io.kestra.plugin.core.runner.Process;
import jakarta.validation.constraints.NotBlank;
@@ -19,13 +18,14 @@ import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.apache.commons.lang3.SystemUtils;
import java.io.IOException;
import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
/**
* Base class for all task runners.
@@ -37,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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)

View File

@@ -47,9 +47,9 @@ 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")
private boolean disabled = false;
@Valid

View File

@@ -4,6 +4,7 @@ import io.kestra.core.models.flows.State;
import io.kestra.core.utils.IdUtils;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.Nullable;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
@@ -46,6 +47,7 @@ public class TriggerContext {
@Nullable
private List<State.Type> stopAfter;
@Schema(defaultValue = "false")
private Boolean disabled = Boolean.FALSE;
protected TriggerContext(TriggerContextBuilder<?, ?> b) {

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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
protected String type;
}

View File

@@ -26,7 +26,6 @@ public interface QueueFactoryInterface {
String SUBFLOWEXECUTIONRESULT_NAMED = "subflowExecutionResultQueue";
String CLUSTER_EVENT_NAMED = "clusterEventQueue";
String SUBFLOWEXECUTIONEND_NAMED = "subflowExecutionEndQueue";
String EXECUTION_RUNNING_NAMED = "executionRunningQueue";
String MULTIPLE_CONDITION_EVENT_NAMED = "multipleConditionEventQueue";
QueueInterface<Execution> execution();
@@ -59,7 +58,5 @@ public interface QueueFactoryInterface {
QueueInterface<SubflowExecutionEnd> subflowExecutionEnd();
QueueInterface<ExecutionRunning> executionRunning();
QueueInterface<MultipleConditionEvent> multipleConditionEvent();
}

View File

@@ -25,8 +25,8 @@ public interface FlowRepositoryInterface {
* Used only if result is used internally and not exposed to the user.
* It is useful when we want to restart/resume a flow.
*/
default Flow findByExecutionWithoutAcl(Execution execution) {
Optional<Flow> find = this.findByIdWithoutAcl(
default FlowWithSource findByExecutionWithoutAcl(Execution execution) {
Optional<FlowWithSource> find = this.findByIdWithSourceWithoutAcl(
execution.getTenantId(),
execution.getNamespace(),
execution.getFlowId(),

View File

@@ -0,0 +1,31 @@
package io.kestra.core.runners;
import io.kestra.core.models.HasUID;
import io.kestra.core.utils.IdUtils;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.With;
@Value
@AllArgsConstructor
@Builder
public class ConcurrencyLimit implements HasUID {
@NotNull
String tenantId;
@NotNull
String namespace;
@NotNull
String flowId;
@With
Integer running;
@Override
public String uid() {
return IdUtils.fromPartsAndSeparator('|', this.tenantId, this.namespace, this.flowId);
}
}

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 = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
@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

@@ -10,7 +10,6 @@ import io.kestra.core.server.ServiceType;
import io.kestra.core.utils.IdUtils;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Duration;
@@ -23,25 +22,24 @@ import java.util.Set;
@KestraTest
public abstract class AbstractServiceUsageReportTest {
@Inject
ServiceUsageReport serviceUsageReport;
@Inject
ServiceInstanceRepositoryInterface serviceInstanceRepository;
@Test
@Disabled
public void shouldGetReport() {
// Given
final LocalDate start = LocalDate.now().withDayOfMonth(1);
final LocalDate start = LocalDate.of(2025, 1, 1);
final LocalDate end = start.withDayOfMonth(start.getMonth().length(start.isLeapYear()));
final ZoneId zoneId = ZoneId.systemDefault();
LocalDate from = start;
int days = 0;
// generate one month of service instance
while (from.toEpochDay() < end.toEpochDay()) {
Instant createAt = from.atStartOfDay(zoneId).toInstant();
Instant updatedAt = from.atStartOfDay(zoneId).plus(Duration.ofHours(10)).toInstant();
@@ -64,14 +62,14 @@ public abstract class AbstractServiceUsageReportTest {
from = from.plusDays(1);
days++;
}
// When
Instant now = end.plusDays(1).atStartOfDay(zoneId).toInstant();
ServiceUsageReport.ServiceUsageEvent event = serviceUsageReport.report(now,
Reportable.TimeInterval.of(start.atStartOfDay(zoneId), end.plusDays(1).atStartOfDay(zoneId))
);
// Then
List<ServiceUsage.DailyServiceStatistics> statistics = event.services().dailyStatistics();
Assertions.assertEquals(ServiceType.values().length - 1, statistics.size());

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,23 @@
id: resume-validate
namespace: io.kestra.tests
labels:
year: 2025
tasks:
- id: pause
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: approved
description: Whether to approve the request
type: BOOLEAN
defaults: true
- id: last
type: io.kestra.plugin.core.debug.Return
format: "{{task.id}} > {{taskrun.startDate}}"
errors:
- id: failed-echo
type: io.kestra.plugin.core.debug.Echo
description: "Log the error"
format: I'm failing {{task.id}}

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

@@ -0,0 +1,16 @@
package io.kestra.runner.h2;
import io.kestra.core.runners.ConcurrencyLimit;
import io.kestra.jdbc.runner.AbstractJdbcConcurrencyLimitStorage;
import io.kestra.repository.h2.H2Repository;
import io.kestra.repository.h2.H2RepositoryEnabled;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
@H2RepositoryEnabled
public class H2ConcurrencyLimitStorage extends AbstractJdbcConcurrencyLimitStorage {
public H2ConcurrencyLimitStorage(@Named("concurrencylimit") H2Repository<ConcurrencyLimit> repository) {
super(repository);
}
}

View File

@@ -1,15 +0,0 @@
package io.kestra.runner.h2;
import io.kestra.core.runners.ExecutionRunning;
import io.kestra.jdbc.runner.AbstractJdbcExecutionRunningStorage;
import io.kestra.repository.h2.H2Repository;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
@H2QueueEnabled
public class H2ExecutionRunningStorage extends AbstractJdbcExecutionRunningStorage {
public H2ExecutionRunningStorage(@Named("executionrunning") H2Repository<ExecutionRunning> repository) {
super(repository);
}
}

View File

@@ -145,14 +145,6 @@ public class H2QueueFactory implements QueueFactoryInterface {
return new H2Queue<>(SubflowExecutionEnd.class, applicationContext);
}
@Override
@Singleton
@Named(QueueFactoryInterface.EXECUTION_RUNNING_NAMED)
@Bean(preDestroy = "close")
public QueueInterface<ExecutionRunning> executionRunning() {
return new H2Queue<>(ExecutionRunning.class, applicationContext);
}
@Override
@Singleton
@Named(QueueFactoryInterface.MULTIPLE_CONDITION_EVENT_NAMED)

View File

@@ -1,2 +0,0 @@
-- We must truncate the table as in 0.24 there was a bug that lead to records not purged in this table
truncate table execution_running;

View File

@@ -1,12 +1,17 @@
CREATE TABLE IF NOT EXISTS execution_running (
CREATE TABLE IF NOT EXISTS concurrency_limit (
"key" VARCHAR(250) NOT NULL PRIMARY KEY,
"value" TEXT NOT NULL,
"tenant_id" VARCHAR(250) GENERATED ALWAYS AS (JQ_STRING("value", '.tenantId')),
"namespace" VARCHAR(150) NOT NULL GENERATED ALWAYS AS (JQ_STRING("value", '.namespace')),
"flow_id" VARCHAR(150) NOT NULL GENERATED ALWAYS AS (JQ_STRING("value", '.flowId'))
);
"flow_id" VARCHAR(150) NOT NULL GENERATED ALWAYS AS (JQ_STRING("value", '.flowId')),
"running" INT NOT NULL GENERATED ALWAYS AS (JQ_INTEGER("value", '.running'))
);
CREATE INDEX IF NOT EXISTS execution_running__flow ON execution_running ("tenant_id", "namespace", "flow_id");
CREATE INDEX IF NOT EXISTS concurrency_limit__flow ON concurrency_limit ("tenant_id", "namespace", "flow_id");
DROP TABLE IF EXISTS execution_running;
DELETE FROM queues WHERE "type" = 'io.kestra.core.runners.ExecutionRunning';
ALTER TABLE queues ALTER COLUMN "type" ENUM(
'io.kestra.core.models.executions.Execution',
@@ -25,5 +30,5 @@ ALTER TABLE queues ALTER COLUMN "type" ENUM(
'io.kestra.core.server.ClusterEvent',
'io.kestra.core.runners.SubflowExecutionEnd',
'io.kestra.core.models.flows.FlowInterface',
'io.kestra.core.runners.ExecutionRunning'
'io.kestra.core.runners.MultipleConditionEvent'
) NOT NULL

View File

@@ -0,0 +1,15 @@
package io.kestra.runner.mysql;
import io.kestra.core.runners.ConcurrencyLimit;
import io.kestra.jdbc.runner.AbstractJdbcConcurrencyLimitStorage;
import io.kestra.repository.mysql.MysqlRepository;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
@MysqlQueueEnabled
public class MysqlConcurrencyLimitStorage extends AbstractJdbcConcurrencyLimitStorage {
public MysqlConcurrencyLimitStorage(@Named("concurrencylimit") MysqlRepository<ConcurrencyLimit> repository) {
super(repository);
}
}

View File

@@ -1,15 +0,0 @@
package io.kestra.runner.mysql;
import io.kestra.core.runners.ExecutionRunning;
import io.kestra.jdbc.runner.AbstractJdbcExecutionRunningStorage;
import io.kestra.repository.mysql.MysqlRepository;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
@MysqlQueueEnabled
public class MysqlExecutionRunningStorage extends AbstractJdbcExecutionRunningStorage {
public MysqlExecutionRunningStorage(@Named("executionrunning") MysqlRepository<ExecutionRunning> repository) {
super(repository);
}
}

View File

@@ -145,14 +145,6 @@ public class MysqlQueueFactory implements QueueFactoryInterface {
return new MysqlQueue<>(SubflowExecutionEnd.class, applicationContext);
}
@Override
@Singleton
@Named(QueueFactoryInterface.EXECUTION_RUNNING_NAMED)
@Bean(preDestroy = "close")
public QueueInterface<ExecutionRunning> executionRunning() {
return new MysqlQueue<>(ExecutionRunning.class, applicationContext);
}
@Override
@Singleton
@Named(QueueFactoryInterface.MULTIPLE_CONDITION_EVENT_NAMED)

View File

@@ -1,2 +0,0 @@
-- We must truncate the table as in 0.24 there was a bug that lead to records not purged in this table
truncate table execution_running;

View File

@@ -1,12 +1,17 @@
CREATE TABLE IF NOT EXISTS execution_running (
CREATE TABLE IF NOT EXISTS concurrency_limit (
`key` VARCHAR(250) NOT NULL PRIMARY KEY,
`value` JSON NOT NULL,
`tenant_id` VARCHAR(250) GENERATED ALWAYS AS (value ->> '$.tenantId') STORED,
`namespace` VARCHAR(150) GENERATED ALWAYS AS (value ->> '$.namespace') STORED NOT NULL,
`flow_id` VARCHAR(150) GENERATED ALWAYS AS (value ->> '$.flowId') STORED NOT NULL,
`running` INT GENERATED ALWAYS AS (value ->> '$.running') STORED NOT NULL,
INDEX ix_flow (tenant_id, namespace, flow_id)
);
DROP TABLE IF EXISTS execution_running;
DELETE FROM queues WHERE type = 'io.kestra.core.runners.ExecutionRunning';
ALTER TABLE queues MODIFY COLUMN `type` ENUM(
'io.kestra.core.models.executions.Execution',
'io.kestra.core.models.templates.Template',
@@ -24,5 +29,5 @@ ALTER TABLE queues MODIFY COLUMN `type` ENUM(
'io.kestra.core.server.ClusterEvent',
'io.kestra.core.runners.SubflowExecutionEnd',
'io.kestra.core.models.flows.FlowInterface',
'io.kestra.core.runners.ExecutionRunning'
) NOT NULL;
'io.kestra.core.runners.MultipleConditionEvent'
) NOT NULL;

View File

@@ -0,0 +1,15 @@
package io.kestra.runner.postgres;
import io.kestra.core.runners.ConcurrencyLimit;
import io.kestra.jdbc.runner.AbstractJdbcConcurrencyLimitStorage;
import io.kestra.repository.postgres.PostgresRepository;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
@PostgresQueueEnabled
public class PostgresConcurrencyLimitStorage extends AbstractJdbcConcurrencyLimitStorage {
public PostgresConcurrencyLimitStorage(@Named("concurrencylimit") PostgresRepository<ConcurrencyLimit> repository) {
super(repository);
}
}

View File

@@ -1,15 +0,0 @@
package io.kestra.runner.postgres;
import io.kestra.core.runners.ExecutionRunning;
import io.kestra.jdbc.runner.AbstractJdbcExecutionRunningStorage;
import io.kestra.repository.postgres.PostgresRepository;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
@PostgresQueueEnabled
public class PostgresExecutionRunningStorage extends AbstractJdbcExecutionRunningStorage {
public PostgresExecutionRunningStorage(@Named("executionrunning") PostgresRepository<ExecutionRunning> repository) {
super(repository);
}
}

View File

@@ -145,14 +145,6 @@ public class PostgresQueueFactory implements QueueFactoryInterface {
return new PostgresQueue<>(SubflowExecutionEnd.class, applicationContext);
}
@Override
@Singleton
@Named(QueueFactoryInterface.EXECUTION_RUNNING_NAMED)
@Bean(preDestroy = "close")
public QueueInterface<ExecutionRunning> executionRunning() {
return new PostgresQueue<>(ExecutionRunning.class, applicationContext);
}
@Override
@Singleton
@Named(QueueFactoryInterface.MULTIPLE_CONDITION_EVENT_NAMED)

View File

@@ -1,2 +0,0 @@
-- We must truncate the table as in 0.24 there was a bug that lead to records not purged in this table
truncate table execution_running;

View File

@@ -1,11 +1,12 @@
CREATE TABLE IF NOT EXISTS execution_running (
CREATE TABLE IF NOT EXISTS concurrency_limit (
key VARCHAR(250) NOT NULL PRIMARY KEY,
value JSONB NOT NULL,
tenant_id VARCHAR(250) GENERATED ALWAYS AS (value ->> 'tenantId') STORED,
namespace VARCHAR(150) NOT NULL GENERATED ALWAYS AS (value ->> 'namespace') STORED,
flow_id VARCHAR(150) NOT NULL GENERATED ALWAYS AS (value ->> 'flowId') STORED
flow_id VARCHAR(150) NOT NULL GENERATED ALWAYS AS (value ->> 'flowId') STORED,
running INT NOT NULL GENERATED ALWAYS AS (CAST(value ->> 'running' AS INTEGER)) STORED
);
CREATE INDEX IF NOT EXISTS execution_running__flow ON execution_running (tenant_id, namespace, flow_id);
CREATE INDEX IF NOT EXISTS concurrency_limit__flow ON concurrency_limit (tenant_id, namespace, flow_id);
ALTER TYPE queue_type ADD VALUE IF NOT EXISTS 'io.kestra.core.runners.ExecutionRunning';
DROP TABLE IF EXISTS execution_running;

View File

@@ -126,9 +126,9 @@ public class JdbcTableConfigsFactory {
}
@Bean
@Named("executionrunning")
public InstantiableJdbcTableConfig executionRunning() {
return new InstantiableJdbcTableConfig("executionrunning", ExecutionRunning.class, "execution_running");
@Named("concurrencylimit")
public InstantiableJdbcTableConfig concurrencyLimit() {
return new InstantiableJdbcTableConfig("concurrencylimit", ConcurrencyLimit.class, "concurrency_limit");
}
public static class InstantiableJdbcTableConfig extends JdbcTableConfig {

View File

@@ -0,0 +1,117 @@
package io.kestra.jdbc.runner;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.runners.ConcurrencyLimit;
import io.kestra.core.runners.ExecutionRunning;
import io.kestra.jdbc.repository.AbstractJdbcRepository;
import org.apache.commons.lang3.tuple.Pair;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.Insert;
import org.jooq.SQLDialect;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
public class AbstractJdbcConcurrencyLimitStorage extends AbstractJdbcRepository {
protected io.kestra.jdbc.AbstractJdbcRepository<ConcurrencyLimit> jdbcRepository;
public AbstractJdbcConcurrencyLimitStorage(io.kestra.jdbc.AbstractJdbcRepository<ConcurrencyLimit> jdbcRepository) {
this.jdbcRepository = jdbcRepository;
}
/**
* Fetch the concurrency limit counter then process the count using the consumer function.
* It locked the raw and is wrapped in a transaction so the consumer should use the provided dslContext for any database access.
* <p>
* Note that to avoid a race when no concurrency limit counter exists, it first always try to insert a 0 counter.
*/
public ExecutionRunning countThenProcess(FlowInterface flow, BiFunction<DSLContext, ConcurrencyLimit, Pair<ExecutionRunning, ConcurrencyLimit>> consumer) {
return this.jdbcRepository
.getDslContextWrapper()
.transactionResult(configuration -> {
var dslContext = DSL.using(configuration);
// Note: ideally, we should emit an INSERT IGNORE or ON CONFLICT DO NOTHING but H2 didn't support it.
// So to avoid the case where no concurrency limit exist and two executors starts a flow concurrently, we select/insert and if the insert fail select again
// Anyway this would only occur once in a flow lifecycle so even if it's not elegant it should work
// But as this pattern didn't work with Postgres, we emit INSERT IGNORE in postgres so we're sure it works their also.
var selected = fetchOne(dslContext, flow).orElseGet(() -> {
try {
var zeroConcurrencyLimit = ConcurrencyLimit.builder()
.tenantId(flow.getTenantId())
.namespace(flow.getNamespace())
.flowId(flow.getId())
.running(0)
.build();
Map<Field<Object>, Object> finalFields = this.jdbcRepository.persistFields(zeroConcurrencyLimit);
var insert = dslContext
.insertInto(this.jdbcRepository.getTable())
.set(field("key"), this.jdbcRepository.key(zeroConcurrencyLimit))
.set(finalFields);
if (dslContext.configuration().dialect().supports(SQLDialect.POSTGRES)) {
insert.onDuplicateKeyIgnore().execute();
} else {
insert.execute();
}
} catch (DataAccessException e) {
// we ignore any constraint violation
}
// refetch to have a lock on it
// at this point we are sure the record is inserted so it should never throw
return fetchOne(dslContext, flow).orElseThrow();
});
var pair = consumer.apply(dslContext, selected);
save(dslContext, pair.getRight());
return pair.getLeft();
});
}
/**
* Decrement the concurrency limit counter.
* Must only be called when a flow having concurrency limit ends.
*/
public void decrement(FlowInterface flow) {
this.jdbcRepository
.getDslContextWrapper()
.transaction(configuration -> {
var dslContext = DSL.using(configuration);
fetchOne(dslContext, flow).ifPresent(
concurrencyLimit -> save(dslContext, concurrencyLimit.withRunning(concurrencyLimit.getRunning() == 0 ? 0 : concurrencyLimit.getRunning() - 1))
);
});
}
/**
* Increment the concurrency limit counter.
* Must only be called when a queued execution is popped, other use cases must pass thought the standard process of creating an execution.
*/
public void increment(DSLContext dslContext, FlowInterface flow) {
fetchOne(dslContext, flow).ifPresent(
concurrencyLimit -> save(dslContext, concurrencyLimit.withRunning(concurrencyLimit.getRunning() + 1))
);
}
private Optional<ConcurrencyLimit> fetchOne(DSLContext dslContext, FlowInterface flow) {
var select = dslContext
.select()
.from(this.jdbcRepository.getTable())
.where(this.buildTenantCondition(flow.getTenantId()))
.and(field("namespace").eq(flow.getNamespace()))
.and(field("flow_id").eq(flow.getId()));
return Optional.ofNullable(select.forUpdate().fetchOne())
.map(record -> this.jdbcRepository.map(record));
}
private void save(DSLContext dslContext, ConcurrencyLimit concurrencyLimit) {
Map<Field<Object>, Object> fields = this.jdbcRepository.persistFields(concurrencyLimit);
this.jdbcRepository.persist(concurrencyLimit, dslContext, fields);
}
}

View File

@@ -11,6 +11,7 @@ import org.jooq.impl.DSL;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRepository {
@@ -25,12 +26,12 @@ public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRep
this.jdbcRepository.persist(executionQueued, dslContext, fields);
}
public void pop(String tenantId, String namespace, String flowId, Consumer<Execution> consumer) {
public void pop(String tenantId, String namespace, String flowId, BiConsumer<DSLContext, Execution> consumer) {
this.jdbcRepository
.getDslContextWrapper()
.transaction(configuration -> {
var select = DSL
.using(configuration)
var dslContext = DSL.using(configuration);
var select = dslContext
.select(AbstractJdbcRepository.field("value"))
.from(this.jdbcRepository.getTable())
.where(buildTenantCondition(tenantId))
@@ -43,7 +44,7 @@ public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRep
Optional<ExecutionQueued> maybeExecution = this.jdbcRepository.fetchOne(select);
if (maybeExecution.isPresent()) {
consumer.accept(maybeExecution.get().getExecution());
consumer.accept(dslContext, maybeExecution.get().getExecution());
this.jdbcRepository.delete(maybeExecution.get());
}
});

View File

@@ -1,83 +0,0 @@
package io.kestra.jdbc.runner;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.runners.ExecutionRunning;
import io.kestra.core.utils.IdUtils;
import io.kestra.jdbc.repository.AbstractJdbcRepository;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.impl.DSL;
import java.util.Map;
import java.util.Optional;
import java.util.function.*;
public class AbstractJdbcExecutionRunningStorage extends AbstractJdbcRepository {
protected io.kestra.jdbc.AbstractJdbcRepository<ExecutionRunning> jdbcRepository;
public AbstractJdbcExecutionRunningStorage(io.kestra.jdbc.AbstractJdbcRepository<ExecutionRunning> jdbcRepository) {
this.jdbcRepository = jdbcRepository;
}
public void save(ExecutionRunning executionRunning) {
jdbcRepository.getDslContextWrapper().transaction(
configuration -> save(DSL.using(configuration), executionRunning)
);
}
public void save(DSLContext dslContext, ExecutionRunning executionRunning) {
Map<Field<Object>, Object> fields = this.jdbcRepository.persistFields(executionRunning);
this.jdbcRepository.persist(executionRunning, dslContext, fields);
}
/**
* Count for running executions then process the count using the consumer function.
* It locked the raw and is wrapped in a transaction so the consumer should use the provided dslContext for any database access.
* <p>
* Note: when there is no execution running, there will be no database locks, so multiple calls will return 0.
* This is only potentially an issue with multiple executor instances when the concurrency limit is set to 1.
*/
public ExecutionRunning countThenProcess(FlowInterface flow, BiFunction<DSLContext, Integer, ExecutionRunning> consumer) {
return this.jdbcRepository
.getDslContextWrapper()
.transactionResult(configuration -> {
var dslContext = DSL.using(configuration);
var select = dslContext
.select(AbstractJdbcRepository.field("value"))
.from(this.jdbcRepository.getTable())
.where(this.buildTenantCondition(flow.getTenantId()))
.and(field("namespace").eq(flow.getNamespace()))
.and(field("flow_id").eq(flow.getId()));
Integer count = select.forUpdate().fetch().size();
return consumer.apply(dslContext, count);
});
}
/**
* Delete the execution running corresponding to the given execution.
* @return true if the execution was deleted, false if it was not existing
*/
public boolean remove(Execution execution) {
return this.jdbcRepository
.getDslContextWrapper()
.transactionResult(configuration -> {
var select = DSL
.using(configuration)
.select(AbstractJdbcRepository.field("value"))
.from(this.jdbcRepository.getTable())
.where(buildTenantCondition(execution.getTenantId()))
.and(field("key").eq(IdUtils.fromPartsAndSeparator('|', execution.getTenantId(), execution.getNamespace(), execution.getFlowId(), execution.getId())))
.forUpdate();
Optional<ExecutionRunning> maybeExecution = this.jdbcRepository.fetchOne(select);
return maybeExecution
.map(executionRunning -> {
this.jdbcRepository.delete(executionRunning);
return true;
})
.orElse(false);
});
}
}

View File

@@ -64,8 +64,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static io.kestra.core.utils.Rethrow.throwConsumer;
import static io.kestra.core.utils.Rethrow.throwFunction;
import static io.kestra.core.utils.Rethrow.*;
@SuppressWarnings("deprecation")
@Singleton
@@ -117,10 +116,6 @@ public class JdbcExecutor implements ExecutorInterface {
@Named(QueueFactoryInterface.CLUSTER_EVENT_NAMED)
private Optional<QueueInterface<ClusterEvent>> clusterEventQueue;
@Inject
@Named(QueueFactoryInterface.EXECUTION_RUNNING_NAMED)
private QueueInterface<ExecutionRunning> executionRunningQueue;
@Inject
@Named(QueueFactoryInterface.MULTIPLE_CONDITION_EVENT_NAMED)
private QueueInterface<MultipleConditionEvent> multipleConditionEventQueue;
@@ -159,7 +154,7 @@ public class JdbcExecutor implements ExecutorInterface {
private AbstractJdbcExecutionQueuedStorage executionQueuedStorage;
@Inject
private AbstractJdbcExecutionRunningStorage executionRunningStorage;
private AbstractJdbcConcurrencyLimitStorage concurrencyLimitStorage;
@Inject
private AbstractJdbcExecutorStateStorage executorStateStorage;
@@ -318,7 +313,6 @@ public class JdbcExecutor implements ExecutorInterface {
this.receiveCancellations.addFirst(this.killQueue.receive(Executor.class, this::killQueue));
this.receiveCancellations.addFirst(this.subflowExecutionResultQueue.receive(Executor.class, this::subflowExecutionResultQueue));
this.receiveCancellations.addFirst(this.subflowExecutionEndQueue.receive(Executor.class, this::subflowExecutionEndQueue));
this.receiveCancellations.addFirst(this.executionRunningQueue.receive(Executor.class, this::executionRunningQueue));
this.receiveCancellations.addFirst(this.multipleConditionEventQueue.receive(Executor.class, this::multipleConditionEventQueue));
this.clusterEventQueue.ifPresent(clusterEventQueueInterface -> this.receiveCancellations.addFirst(clusterEventQueueInterface.receive(this::clusterEventQueue)));
@@ -603,11 +597,23 @@ public class JdbcExecutor implements ExecutorInterface {
.concurrencyState(ExecutionRunning.ConcurrencyState.CREATED)
.build();
executionRunningQueue.emit(executionRunning);
return Pair.of(
executor,
executorState
);
ExecutionRunning processed = concurrencyLimitStorage.countThenProcess(flow, (dslContext, concurrencyLimit) -> {
ExecutionRunning computed = executorService.processExecutionRunning(flow, concurrencyLimit.getRunning(), executionRunning.withExecution(execution)); // be sure that the execution running contains the latest value of the execution
if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.RUNNING && !computed.getExecution().getState().isTerminated()) {
return Pair.of(computed, concurrencyLimit.withRunning(concurrencyLimit.getRunning() + 1));
} else if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.QUEUED) {
executionQueuedStorage.save(dslContext, ExecutionQueued.fromExecutionRunning(computed));
}
return Pair.of(computed, concurrencyLimit);
});
// if the execution is queued or terminated due to concurrency limit, we stop here
if (processed.getExecution().getState().isTerminated() || processed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.QUEUED) {
return Pair.of(
executor.withExecution(processed.getExecution(), "handleConcurrencyLimit"),
executorState
);
}
}
// handle execution changed SLA
@@ -1008,37 +1014,6 @@ public class JdbcExecutor implements ExecutorInterface {
}
}
private void executionRunningQueue(Either<ExecutionRunning, DeserializationException> either) {
if (either.isRight()) {
log.error("Unable to deserialize a running execution: {}", either.getRight().getMessage());
return;
}
ExecutionRunning executionRunning = either.getLeft();
// we need to update the execution after applying concurrency limit so we use the lock for that
Executor executor = executionRepository.lock(executionRunning.getExecution().getId(), pair -> {
Execution execution = pair.getLeft();
Executor newExecutor = new Executor(execution, null);
FlowInterface flow = flowMetaStore.findByExecution(execution).orElseThrow();
ExecutionRunning processed = executionRunningStorage.countThenProcess(flow, (dslContext, count) -> {
ExecutionRunning computed = executorService.processExecutionRunning(flow, count, executionRunning.withExecution(execution)); // be sure that the execution running contains the latest value of the execution
if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.RUNNING && !computed.getExecution().getState().isTerminated()) {
executionRunningStorage.save(dslContext, computed);
} else if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.QUEUED) {
executionQueuedStorage.save(dslContext, ExecutionQueued.fromExecutionRunning(computed));
}
return computed;
});
return Pair.of(
newExecutor.withExecution(processed.getExecution(), "handleExecutionRunning"),
pair.getRight()
);
});
toExecution(executor);
}
private Executor killingOrAfterKillState(final String executionId, Optional<State.Type> afterKillState) {
return executionRepository.lock(executionId, pair -> {
Execution currentExecution = pair.getLeft();
@@ -1152,31 +1127,31 @@ public class JdbcExecutor implements ExecutorInterface {
// check if there exist a queued execution and submit it to the execution queue
if (executor.getFlow().getConcurrency() != null) {
// purge execution running
boolean hasExecutionRunning = executionRunningStorage.remove(execution);
// decrement execution concurrency limit
// if an execution was queued but never running, it would have never been counted inside the concurrency limit and should not lead to popping a new queued execution
// this could only happen for KILLED execution.
boolean queuedThenKilled = execution.getState().getCurrent() == State.Type.KILLED
&& execution.getState().getHistories().stream().anyMatch(h -> h.getState().isQueued())
&& execution.getState().getHistories().stream().noneMatch(h -> h.getState().isRunning());
if (!queuedThenKilled) {
concurrencyLimitStorage.decrement(executor.getFlow());
// some execution may have concurrency limit but no execution running: for ex QUEUED -> KILLED, in this case we should not pop any execution
if (hasExecutionRunning && executor.getFlow().getConcurrency().getBehavior() == Concurrency.Behavior.QUEUE) {
executionQueuedStorage.pop(executor.getFlow().getTenantId(),
executor.getFlow().getNamespace(),
executor.getFlow().getId(),
throwConsumer(queued -> {
var newExecution = queued.withState(State.Type.RUNNING);
ExecutionRunning executionRunning = ExecutionRunning.builder()
.tenantId(newExecution.getTenantId())
.namespace(newExecution.getNamespace())
.flowId(newExecution.getFlowId())
.execution(newExecution)
.concurrencyState(ExecutionRunning.ConcurrencyState.RUNNING)
.build();
executionRunningStorage.save(executionRunning);
executionQueue.emit(newExecution);
metricRegistry.counter(MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT, MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT_DESCRIPTION, metricRegistry.tags(newExecution)).increment();
if (executor.getFlow().getConcurrency().getBehavior() == Concurrency.Behavior.QUEUE) {
var finalFlow = executor.getFlow();
executionQueuedStorage.pop(executor.getFlow().getTenantId(),
executor.getFlow().getNamespace(),
executor.getFlow().getId(),
throwBiConsumer((dslContext, queued) -> {
var newExecution = queued.withState(State.Type.RUNNING);
concurrencyLimitStorage.increment(dslContext, finalFlow);
executionQueue.emit(newExecution);
metricRegistry.counter(MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT, MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT_DESCRIPTION, metricRegistry.tags(newExecution)).increment();
// process flow triggers to allow listening on RUNNING state after a QUEUED state
processFlowTriggers(newExecution);
})
);
// process flow triggers to allow listening on RUNNING state after a QUEUED state
processFlowTriggers(newExecution);
})
);
}
}
}
@@ -1229,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)));
}
@@ -1295,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,

Some files were not shown because too many files have changed in this diff Show More