mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
105 Commits
no-default
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
485f9a3669 | ||
|
|
ae7bb88ff0 | ||
|
|
8f29a72df7 | ||
|
|
fc8732f96e | ||
|
|
14f4449d99 | ||
|
|
dd80a91ab3 | ||
|
|
840f010921 | ||
|
|
8462b178cb | ||
|
|
901625786d | ||
|
|
4def8c5764 | ||
|
|
65a204356c | ||
|
|
73e3fd08e9 | ||
|
|
0665b52014 | ||
|
|
781f9dc8d8 | ||
|
|
92e4570158 | ||
|
|
ce47b4ee5e | ||
|
|
69193c6096 | ||
|
|
9d437957fa | ||
|
|
dfea86fb07 | ||
|
|
6fee99a78a | ||
|
|
2c766a5497 | ||
|
|
959737f545 | ||
|
|
2c1b6ffe3c | ||
|
|
9b819f6925 | ||
|
|
a81c0a5737 | ||
|
|
ebcb0bd2a2 | ||
|
|
2a26c415bf | ||
|
|
1e06b9f1c0 | ||
|
|
0b64da5e84 | ||
|
|
d95b65082f | ||
|
|
1bba3e4035 | ||
|
|
efe0c6e1e4 | ||
|
|
fce7ec135e | ||
|
|
5a2456716f | ||
|
|
f43f8c2dc0 | ||
|
|
94811d4e06 | ||
|
|
b7166488be | ||
|
|
78066fef62 | ||
|
|
c70abcb85f | ||
|
|
774a4ef7a1 | ||
|
|
a48fd02ed7 | ||
|
|
030d948521 | ||
|
|
c4aa6c1097 | ||
|
|
3d1a3d0e7a | ||
|
|
30ab030244 | ||
|
|
cc083385f0 | ||
|
|
c14462f5fa | ||
|
|
d6e470d788 | ||
|
|
58ae507e21 | ||
|
|
71110ccfc3 | ||
|
|
fdcc07b546 | ||
|
|
221236e079 | ||
|
|
d14deaceb0 | ||
|
|
bfdc48bbbe | ||
|
|
e6b2f1f79a | ||
|
|
0632052837 | ||
|
|
3df9d49aa0 | ||
|
|
318f2b7d5a | ||
|
|
800970a88f | ||
|
|
f717063a83 | ||
|
|
9a7fb64943 | ||
|
|
45bddb8d09 | ||
|
|
881b009d9b | ||
|
|
ab818713f6 | ||
|
|
d68ffa3109 | ||
|
|
addd76f9bb | ||
|
|
6be939c1bd | ||
|
|
4ea876d3fe | ||
|
|
42d8005eff | ||
|
|
58a360fae0 | ||
|
|
49b647e1fc | ||
|
|
b314fc393b | ||
|
|
0a298cad17 | ||
|
|
61170e6067 | ||
|
|
cfbffad31a | ||
|
|
41e2dac4ca | ||
|
|
0fa8386cb3 | ||
|
|
0f45c009ab | ||
|
|
b86177f329 | ||
|
|
fe396c455b | ||
|
|
0830e11645 | ||
|
|
4d7f6b2bb1 | ||
|
|
955c6b728b | ||
|
|
d2d26351bd | ||
|
|
f14b638f73 | ||
|
|
259b5b5282 | ||
|
|
b1c50374b4 | ||
|
|
de2d923bd4 | ||
|
|
89c76208a4 | ||
|
|
12eb8367ec | ||
|
|
7421f1e93d | ||
|
|
5642a53893 | ||
|
|
d5a2f4430f | ||
|
|
0299e0d5ce | ||
|
|
c69ac99a7f | ||
|
|
fd1b4d5234 | ||
|
|
4ef3600954 | ||
|
|
366df0f37f | ||
|
|
5e87655a0e | ||
|
|
0f01699d27 | ||
|
|
0794b2bf8e | ||
|
|
0becaa0b97 | ||
|
|
7d3bb34fd4 | ||
|
|
b8c55baff1 | ||
|
|
1f8e5ad18e |
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
needs: [tests]
|
||||
if: "!startsWith(github.ref, 'refs/heads/releases')"
|
||||
uses: ./.github/workflows/workflow-release.yml
|
||||
with:
|
||||
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
|
||||
7
.github/workflows/pull-request.yml
vendored
7
.github/workflows/pull-request.yml
vendored
@@ -10,7 +10,11 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ********************************************************************************************************************
|
||||
# File changes detection
|
||||
# ********************************************************************************************************************
|
||||
file-changes:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
name: File changes detection
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
@@ -29,6 +33,9 @@ jobs:
|
||||
- '!{ui,.github}/**'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ********************************************************************************************************************
|
||||
# Tests
|
||||
# ********************************************************************************************************************
|
||||
frontend:
|
||||
name: Frontend - Tests
|
||||
needs: [file-changes]
|
||||
|
||||
3
.github/workflows/setversion-tag.yml
vendored
3
.github/workflows/setversion-tag.yml
vendored
@@ -23,12 +23,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_BRANCH="{{ github.ref }}"
|
||||
|
||||
# Extract the major and minor versions
|
||||
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
||||
RELEASE_BRANCH="refs/heads/releases/v${BASE_VERSION}.x"
|
||||
|
||||
CURRENT_BRANCH="$GITHUB_REF"
|
||||
if ! [[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]]; then
|
||||
echo "Invalid release branch. Expected $RELEASE_BRANCH, was $CURRENT_BRANCH"
|
||||
exit 1
|
||||
|
||||
1
.github/workflows/workflow-backend-test.yml
vendored
1
.github/workflows/workflow-backend-test.yml
vendored
@@ -68,6 +68,7 @@ jobs:
|
||||
list-suites: 'failed'
|
||||
list-tests: 'failed'
|
||||
fail-on-error: 'false'
|
||||
token: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
||||
|
||||
# Sonar
|
||||
- name: Test - Analyze with Sonar
|
||||
|
||||
26
.github/workflows/workflow-github-release.yml
vendored
26
.github/workflows/workflow-github-release.yml
vendored
@@ -20,17 +20,23 @@ jobs:
|
||||
name: exe
|
||||
path: build/executable
|
||||
|
||||
# GitHub Release
|
||||
- name: GitHub - Create release
|
||||
id: create_github_release
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
continue-on-error: true
|
||||
# Checkout GitHub Actions
|
||||
- name: Checkout - Actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
files: |
|
||||
build/executable/*
|
||||
repository: kestra-io/actions
|
||||
sparse-checkout-cone-mode: true
|
||||
path: actions
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
|
||||
# GitHub Release
|
||||
- name: Create GitHub release
|
||||
uses: ./actions/.github/actions/github-release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||
|
||||
# Trigger gha workflow to bump helm chart version
|
||||
- name: GitHub - Trigger the Helm chart version bump
|
||||
|
||||
62
.github/workflows/workflow-publish-docker.yml
vendored
62
.github/workflows/workflow-publish-docker.yml
vendored
@@ -8,6 +8,11 @@ on:
|
||||
default: 'LATEST'
|
||||
required: false
|
||||
type: string
|
||||
force-download-artifact:
|
||||
description: 'Force download artifact'
|
||||
required: false
|
||||
type: string
|
||||
default: "true"
|
||||
workflow_call:
|
||||
inputs:
|
||||
plugin-version:
|
||||
@@ -15,6 +20,11 @@ on:
|
||||
default: 'LATEST'
|
||||
required: false
|
||||
type: string
|
||||
force-download-artifact:
|
||||
description: 'Force download artifact'
|
||||
required: false
|
||||
type: string
|
||||
default: "true"
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
description: "The Dockerhub username."
|
||||
@@ -24,19 +34,38 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
|
||||
# ********************************************************************************************************************
|
||||
# Build
|
||||
# ********************************************************************************************************************
|
||||
build-artifacts:
|
||||
name: Build Artifacts
|
||||
if: ${{ github.event.inputs.force-download-artifact == 'true' }}
|
||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
||||
with:
|
||||
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
# ********************************************************************************************************************
|
||||
# Docker
|
||||
# ********************************************************************************************************************
|
||||
publish:
|
||||
name: Publish - Docker
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-artifacts
|
||||
if: |
|
||||
always() &&
|
||||
(needs.build-artifacts.result == 'success' ||
|
||||
github.event.inputs.force-download-artifact != 'true')
|
||||
env:
|
||||
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
strategy:
|
||||
matrix:
|
||||
image:
|
||||
- tag: ${{ needs.build-artifacts.outputs.docker-tag }}-no-plugins
|
||||
- tag: -no-plugins
|
||||
packages: jattach
|
||||
plugins: false
|
||||
python-libraries: ""
|
||||
|
||||
- tag: ${{ needs.build-artifacts.outputs.docker-tag }}
|
||||
plugins: ${{ needs.build-artifacts.outputs.plugins }}
|
||||
- tag: ""
|
||||
plugins: true
|
||||
packages: python3 python3-venv python-is-python3 python3-pip nodejs npm curl zip unzip jattach
|
||||
python-libraries: kestra
|
||||
steps:
|
||||
@@ -62,17 +91,34 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
|
||||
# # Get Plugins List
|
||||
- name: Plugins - Get List
|
||||
uses: ./.github/actions/plugins-list
|
||||
id: plugins-list
|
||||
if: ${{ matrix.image.plugins}}
|
||||
with:
|
||||
plugin-version: ${{ env.PLUGIN_VERSION }}
|
||||
|
||||
# Vars
|
||||
- name: Docker - Set image name
|
||||
- name: Docker - Set variables
|
||||
shell: bash
|
||||
id: vars
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/*/}
|
||||
if [[ $TAG = "master" || $TAG == v* ]]; then
|
||||
PLUGINS="${{ matrix.image.plugins == true && steps.plugins-list.outputs.plugins || '' }}"
|
||||
if [[ $TAG == v* ]]; then
|
||||
TAG="${TAG}";
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
elif [[ $TAG = "develop" ]]; then
|
||||
TAG="develop";
|
||||
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots $PLUGINS" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
TAG="build-${{ github.run_id }}";
|
||||
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots $PLUGINS" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "tag=${TAG}${{ matrix.image.tag }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Build Docker Image
|
||||
- name: Artifacts - Download executable
|
||||
@@ -92,7 +138,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: kestra/kestra:${{ matrix.image.tag }}
|
||||
tags: kestra/kestra:${{ steps.vars.outputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
KESTRA_PLUGINS=${{ steps.vars.outputs.plugins }}
|
||||
|
||||
7
.github/workflows/workflow-release.yml
vendored
7
.github/workflows/workflow-release.yml
vendored
@@ -8,6 +8,11 @@ on:
|
||||
default: 'LATEST'
|
||||
required: false
|
||||
type: string
|
||||
publish-docker:
|
||||
description: "Publish Docker image"
|
||||
default: 'false'
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
plugin-version:
|
||||
@@ -48,7 +53,9 @@ jobs:
|
||||
name: Publish Docker
|
||||
needs: build-artifacts
|
||||
uses: ./.github/workflows/workflow-publish-docker.yml
|
||||
if: startsWith(github.ref, 'refs/heads/develop') || github.event.inputs.publish-docker == 'true'
|
||||
with:
|
||||
force-download-artifact: 'false'
|
||||
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
||||
@@ -21,7 +21,7 @@ plugins {
|
||||
|
||||
// test
|
||||
id "com.adarshr.test-logger" version "4.0.0"
|
||||
id "org.sonarqube" version "6.0.1.5171"
|
||||
id "org.sonarqube" version "6.1.0.5360"
|
||||
id 'jacoco-report-aggregation'
|
||||
|
||||
// helper
|
||||
@@ -39,7 +39,7 @@ plugins {
|
||||
id 'ru.vyarus.github-info' version '2.0.0' apply false
|
||||
|
||||
// OWASP dependency check
|
||||
id "org.owasp.dependencycheck" version "12.1.0" apply false
|
||||
id "org.owasp.dependencycheck" version "12.1.1" apply false
|
||||
}
|
||||
|
||||
idea {
|
||||
|
||||
@@ -46,8 +46,18 @@ public abstract class AbstractApiCommand extends AbstractCommand {
|
||||
@Nullable
|
||||
private HttpClientConfiguration httpClientConfiguration;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected boolean loadExternalPlugins() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected DefaultHttpClient client() throws URISyntaxException {
|
||||
DefaultHttpClient defaultHttpClient = new DefaultHttpClient(server.toURI(), httpClientConfiguration != null ? httpClientConfiguration : new DefaultHttpClientConfiguration());
|
||||
DefaultHttpClient defaultHttpClient = DefaultHttpClient.builder()
|
||||
.uri(server.toURI())
|
||||
.configuration(httpClientConfiguration != null ? httpClientConfiguration : new DefaultHttpClientConfiguration())
|
||||
.build();
|
||||
MessageBodyHandlerRegistry defaultHandlerRegistry = defaultHttpClient.getHandlerRegistry();
|
||||
if (defaultHandlerRegistry instanceof ContextlessMessageBodyHandlerRegistry modifiableRegistry) {
|
||||
modifiableRegistry.add(MediaType.TEXT_JSON_TYPE, new NettyJsonHandler<>(JsonMapper.createDefault()));
|
||||
|
||||
@@ -31,6 +31,12 @@ public abstract class AbstractValidateCommand extends AbstractApiCommand {
|
||||
@CommandLine.Parameters(index = "0", description = "the directory containing files to check")
|
||||
protected Path directory;
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
protected boolean loadExternalPlugins() {
|
||||
return local;
|
||||
}
|
||||
|
||||
public static void handleException(ConstraintViolationException e, String resource) {
|
||||
stdErr("\t@|fg(red) Unable to parse {0} due to the following error(s):|@", resource);
|
||||
e.getConstraintViolations()
|
||||
@@ -68,10 +74,9 @@ public abstract class AbstractValidateCommand extends AbstractApiCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// bug in micronaut, we can't inject YamlFlowParser & ModelValidator, so we inject from implementation
|
||||
// bug in micronaut, we can't inject ModelValidator, so we inject from implementation
|
||||
public Integer call(
|
||||
Class<?> cls,
|
||||
YamlParser yamlParser,
|
||||
ModelValidator modelValidator,
|
||||
Function<Object, String> identity,
|
||||
Function<Object, List<String>> warningsFunction,
|
||||
@@ -88,7 +93,7 @@ public abstract class AbstractValidateCommand extends AbstractApiCommand {
|
||||
.filter(YamlParser::isValidExtension)
|
||||
.forEach(path -> {
|
||||
try {
|
||||
Object parse = yamlParser.parse(path.toFile(), cls);
|
||||
Object parse = YamlParser.parse(path.toFile(), cls);
|
||||
modelValidator.validate(parse);
|
||||
stdOut("@|green \u2713|@ - " + identity.apply(parse));
|
||||
List<String> warnings = warningsFunction.apply(parse);
|
||||
|
||||
@@ -29,8 +29,7 @@ public class FlowDotCommand extends AbstractCommand {
|
||||
public Integer call() throws Exception {
|
||||
super.call();
|
||||
|
||||
YamlParser parser = applicationContext.getBean(YamlParser.class);
|
||||
Flow flow = parser.parse(file.toFile(), Flow.class);
|
||||
Flow flow = YamlParser.parse(file.toFile(), Flow.class);
|
||||
|
||||
GraphCluster graph = GraphUtils.of(flow, null);
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ public class FlowExpandCommand extends AbstractCommand {
|
||||
@CommandLine.Parameters(index = "0", description = "The flow file to expand")
|
||||
private Path file;
|
||||
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
|
||||
@@ -31,7 +28,7 @@ public class FlowExpandCommand extends AbstractCommand {
|
||||
super.call();
|
||||
stdErr("Warning, this functionality is deprecated and will be removed at some point.");
|
||||
String content = IncludeHelperExpander.expand(Files.readString(file), file.getParent());
|
||||
Flow flow = yamlParser.parse(content, Flow.class);
|
||||
Flow flow = YamlParser.parse(content, Flow.class);
|
||||
modelValidator.validate(flow);
|
||||
stdOut(content);
|
||||
return 0;
|
||||
|
||||
@@ -87,4 +87,9 @@ public class FlowUpdatesCommand extends AbstractApiCommand {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean loadExternalPlugins() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package io.kestra.cli.commands.flows;
|
||||
|
||||
import io.kestra.cli.AbstractValidateCommand;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.services.FlowService;
|
||||
import jakarta.inject.Inject;
|
||||
import picocli.CommandLine;
|
||||
@@ -16,8 +15,6 @@ import java.util.List;
|
||||
description = "Validate a flow"
|
||||
)
|
||||
public class FlowValidateCommand extends AbstractValidateCommand {
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
@@ -28,23 +25,22 @@ public class FlowValidateCommand extends AbstractValidateCommand {
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
return this.call(
|
||||
Flow.class,
|
||||
yamlParser,
|
||||
FlowWithSource.class,
|
||||
modelValidator,
|
||||
(Object object) -> {
|
||||
Flow flow = (Flow) object;
|
||||
FlowWithSource flow = (FlowWithSource) object;
|
||||
return flow.getNamespace() + " / " + flow.getId();
|
||||
},
|
||||
(Object object) -> {
|
||||
Flow flow = (Flow) object;
|
||||
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, this.tenantId));
|
||||
return warnings;
|
||||
},
|
||||
(Object object) -> {
|
||||
Flow flow = (Flow) object;
|
||||
return flowService.relocations(flow.generateSource()).stream().map(relocation -> relocation.from() + " is replaced by " + relocation.to()).toList();
|
||||
FlowWithSource flow = (FlowWithSource) object;
|
||||
return flowService.relocations(flow.sourceOrGenerateIfNull()).stream().map(relocation -> relocation.from() + " is replaced by " + relocation.to()).toList();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import io.micronaut.http.MediaType;
|
||||
import io.micronaut.http.MutableHttpRequest;
|
||||
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
||||
import io.micronaut.http.client.netty.DefaultHttpClient;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
@@ -27,8 +26,6 @@ import java.util.List;
|
||||
)
|
||||
@Slf4j
|
||||
public class FlowNamespaceUpdateCommand extends AbstractServiceNamespaceUpdateCommand {
|
||||
@Inject
|
||||
public YamlParser yamlParser;
|
||||
|
||||
@CommandLine.Option(names = {"--override-namespaces"}, negatable = true, description = "Replace namespace of all flows by the one provided")
|
||||
public boolean override = false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import picocli.CommandLine.Command;
|
||||
mixinStandardHelpOptions = true,
|
||||
subcommands = {
|
||||
PluginInstallCommand.class,
|
||||
PluginUninstallCommand.class,
|
||||
PluginListCommand.class,
|
||||
PluginDocCommand.class,
|
||||
PluginSearchCommand.class
|
||||
|
||||
@@ -17,10 +17,10 @@ import java.util.List;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "uninstall",
|
||||
description = "uninstall a plugin"
|
||||
description = "Uninstall plugins"
|
||||
)
|
||||
public class PluginUninstallCommand extends AbstractCommand {
|
||||
@Parameters(index = "0..*", description = "the plugins to uninstall")
|
||||
@Parameters(index = "0..*", description = "The plugins to uninstall. Represented as Maven artifact coordinates (i.e., <groupId>:<artifactId>:(<version>|LATEST)")
|
||||
List<String> dependencies = new ArrayList<>();
|
||||
|
||||
@Spec
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.servers;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.kestra.cli.services.FileChangedEventListener;
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import io.kestra.core.models.ServerType;
|
||||
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||
import io.kestra.core.runners.StandAloneRunner;
|
||||
@@ -88,9 +89,10 @@ public class StandAloneCommand extends AbstractServerCommand {
|
||||
this.skipExecutionService.setSkipFlows(skipFlows);
|
||||
this.skipExecutionService.setSkipNamespaces(skipNamespaces);
|
||||
this.skipExecutionService.setSkipTenants(skipTenants);
|
||||
|
||||
this.startExecutorService.applyOptions(startExecutors, notStartExecutors);
|
||||
|
||||
KestraContext.getContext().injectWorkerConfigs(workerThread, null);
|
||||
|
||||
super.call();
|
||||
|
||||
if (flowPath != null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.cli.commands.servers;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import io.kestra.core.models.ServerType;
|
||||
import io.kestra.core.runners.Worker;
|
||||
import io.kestra.core.utils.Await;
|
||||
@@ -36,7 +37,11 @@ public class WorkerCommand extends AbstractServerCommand {
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
|
||||
KestraContext.getContext().injectWorkerConfigs(thread, workerGroupKey);
|
||||
|
||||
super.call();
|
||||
|
||||
if (this.workerGroupKey != null && !this.workerGroupKey.matches("[a-zA-Z0-9_-]+")) {
|
||||
throw new IllegalArgumentException("The --worker-group option must match the [a-zA-Z0-9_-]+ pattern");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.sys;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -9,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "reindex",
|
||||
@@ -33,8 +35,8 @@ public class ReindexCommand extends AbstractCommand {
|
||||
List<Flow> allFlow = flowRepository.findAllForAllTenants();
|
||||
allFlow.stream()
|
||||
.map(flow -> flowRepository.findByIdWithSource(flow.getTenantId(), flow.getNamespace(), flow.getId()).orElse(null))
|
||||
.filter(flow -> flow != null)
|
||||
.forEach(flow -> flowRepository.update(flow.toFlow(), flow.toFlow(), flow.getSource(), flow.toFlow()));
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(flow -> flowRepository.update(GenericFlow.of(flow), flow));
|
||||
|
||||
stdOut("Successfully reindex " + allFlow.size() + " flow(s).");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.kestra.cli.AbstractValidateCommand;
|
||||
import io.kestra.core.models.templates.Template;
|
||||
import io.kestra.core.models.templates.TemplateEnabled;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import jakarta.inject.Inject;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@@ -16,8 +15,6 @@ import java.util.Collections;
|
||||
)
|
||||
@TemplateEnabled
|
||||
public class TemplateValidateCommand extends AbstractValidateCommand {
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
@@ -26,7 +23,6 @@ public class TemplateValidateCommand extends AbstractValidateCommand {
|
||||
public Integer call() throws Exception {
|
||||
return this.call(
|
||||
Template.class,
|
||||
yamlParser,
|
||||
modelValidator,
|
||||
(Object object) -> {
|
||||
Template template = (Template) object;
|
||||
|
||||
@@ -10,7 +10,6 @@ import io.micronaut.http.HttpRequest;
|
||||
import io.micronaut.http.MutableHttpRequest;
|
||||
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
||||
import io.micronaut.http.client.netty.DefaultHttpClient;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@@ -27,8 +26,6 @@ import jakarta.validation.ConstraintViolationException;
|
||||
@Slf4j
|
||||
@TemplateEnabled
|
||||
public class TemplateNamespaceUpdateCommand extends AbstractServiceNamespaceUpdateCommand {
|
||||
@Inject
|
||||
public YamlParser yamlParser;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
@@ -38,7 +35,7 @@ public class TemplateNamespaceUpdateCommand extends AbstractServiceNamespaceUpda
|
||||
List<Template> templates = files
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(YamlParser::isValidExtension)
|
||||
.map(path -> yamlParser.parse(path.toFile(), Template.class))
|
||||
.map(path -> YamlParser.parse(path.toFile(), Template.class))
|
||||
.toList();
|
||||
|
||||
if (templates.isEmpty()) {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package io.kestra.cli.services;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.exceptions.DeserializationException;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithPath;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.services.FlowListenersInterface;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.*;
|
||||
@@ -40,9 +41,6 @@ public class FileChangedEventListener {
|
||||
@Inject
|
||||
private PluginDefaultService pluginDefaultService;
|
||||
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
|
||||
@@ -59,7 +57,6 @@ public class FileChangedEventListener {
|
||||
|
||||
private boolean isStarted = false;
|
||||
|
||||
|
||||
@Inject
|
||||
public FileChangedEventListener(@Nullable FileWatchConfiguration fileWatchConfiguration, @Nullable WatchService watchService) {
|
||||
this.fileWatchConfiguration = fileWatchConfiguration;
|
||||
@@ -68,7 +65,7 @@ public class FileChangedEventListener {
|
||||
|
||||
public void startListeningFromConfig() throws IOException, InterruptedException {
|
||||
if (fileWatchConfiguration != null && fileWatchConfiguration.isEnabled()) {
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface, pluginDefaultService);
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface);
|
||||
List<Path> paths = fileWatchConfiguration.getPaths();
|
||||
this.setup(paths);
|
||||
|
||||
@@ -76,7 +73,7 @@ public class FileChangedEventListener {
|
||||
// Init existing flows not already in files
|
||||
flowListeners.listen(flows -> {
|
||||
if (!isStarted) {
|
||||
for (FlowWithSource flow : flows) {
|
||||
for (FlowInterface flow : flows) {
|
||||
if (this.flows.stream().noneMatch(flowWithPath -> flowWithPath.uidWithoutRevision().equals(flow.uidWithoutRevision()))) {
|
||||
flowToFile(flow, this.buildPath(flow));
|
||||
this.flows.add(FlowWithPath.of(flow, this.buildPath(flow).toString()));
|
||||
@@ -137,7 +134,7 @@ public class FileChangedEventListener {
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
Optional<FlowWithSource> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
@@ -156,7 +153,7 @@ public class FileChangedEventListener {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
flowFilesManager.createOrUpdateFlow(GenericFlow.fromYaml(tenantId, content));
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
}
|
||||
|
||||
@@ -207,11 +204,11 @@ public class FileChangedEventListener {
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (file.toString().endsWith(".yml") || file.toString().endsWith(".yaml")) {
|
||||
String content = Files.readString(file, Charset.defaultCharset());
|
||||
Optional<Flow> flow = parseFlow(content, file);
|
||||
Optional<FlowWithSource> flow = parseFlow(content, file);
|
||||
|
||||
if (flow.isPresent() && flows.stream().noneMatch(flowWithPath -> flowWithPath.uidWithoutRevision().equals(flow.get().uidWithoutRevision()))) {
|
||||
flows.add(FlowWithPath.of(flow.get(), file.toString()));
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
flowFilesManager.createOrUpdateFlow(GenericFlow.fromYaml(tenantId, content));
|
||||
}
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
@@ -223,27 +220,25 @@ public class FileChangedEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void flowToFile(FlowWithSource flow, Path path) {
|
||||
private void flowToFile(FlowInterface flow, Path path) {
|
||||
Path defaultPath = path != null ? path : this.buildPath(flow);
|
||||
|
||||
try {
|
||||
Files.writeString(defaultPath, flow.getSource());
|
||||
Files.writeString(defaultPath, flow.source());
|
||||
log.info("Flow {} has been written to file {}", flow.getId(), defaultPath);
|
||||
} catch (IOException e) {
|
||||
log.error("Error writing file: {}", defaultPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Flow> parseFlow(String content, Path entry) {
|
||||
private Optional<FlowWithSource> parseFlow(String content, Path entry) {
|
||||
try {
|
||||
Flow flow = yamlParser.parse(content, Flow.class);
|
||||
FlowWithSource withPluginDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
modelValidator.validate(withPluginDefault);
|
||||
FlowWithSource flow = pluginDefaultService.parseFlowWithAllDefaults(tenantId, content, false);
|
||||
modelValidator.validate(flow);
|
||||
return Optional.of(flow);
|
||||
} catch (ConstraintViolationException e) {
|
||||
} catch (DeserializationException | ConstraintViolationException e) {
|
||||
log.warn("Error while parsing flow: {}", entry, e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -259,7 +254,7 @@ public class FileChangedEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
private Path buildPath(Flow flow) {
|
||||
private Path buildPath(FlowInterface flow) {
|
||||
return fileWatchConfiguration.getPaths().getFirst().resolve(flow.uidWithoutRevision() + ".yml");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package io.kestra.cli.services;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
|
||||
public interface FlowFilesManager {
|
||||
|
||||
FlowWithSource createOrUpdateFlow(Flow flow, String content);
|
||||
FlowWithSource createOrUpdateFlow(GenericFlow flow);
|
||||
|
||||
void deleteFlow(FlowWithSource toDelete);
|
||||
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
package io.kestra.cli.services;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class LocalFlowFileWatcher implements FlowFilesManager {
|
||||
private final FlowRepositoryInterface flowRepository;
|
||||
private final PluginDefaultService pluginDefaultService;
|
||||
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepository, PluginDefaultService pluginDefaultService) {
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepository) {
|
||||
this.flowRepository = flowRepository;
|
||||
this.pluginDefaultService = pluginDefaultService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowWithSource createOrUpdateFlow(Flow flow, String content) {
|
||||
FlowWithSource withDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
public FlowWithSource createOrUpdateFlow(final GenericFlow flow) {
|
||||
return flowRepository.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepository.update(flow, previous, content, withDefault))
|
||||
.orElseGet(() -> flowRepository.create(flow, content, withDefault));
|
||||
.map(previous -> flowRepository.update(flow, previous))
|
||||
.orElseGet(() -> flowRepository.create(flow));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,6 +32,8 @@ class FlowExportCommandTest {
|
||||
|
||||
// we use the update command to add flows to extract
|
||||
String[] updateArgs = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -44,6 +46,8 @@ class FlowExportCommandTest {
|
||||
|
||||
// then we export them
|
||||
String[] exportArgs = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
|
||||
@@ -28,6 +28,8 @@ class FlowUpdatesCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -41,6 +43,8 @@ class FlowUpdatesCommandTest {
|
||||
out.reset();
|
||||
|
||||
args = new String[]{
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -70,6 +74,8 @@ class FlowUpdatesCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -84,6 +90,8 @@ class FlowUpdatesCommandTest {
|
||||
|
||||
// no "delete" arg should behave as no-delete
|
||||
args = new String[]{
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -96,6 +104,8 @@ class FlowUpdatesCommandTest {
|
||||
out.reset();
|
||||
|
||||
args = new String[]{
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -121,6 +131,8 @@ class FlowUpdatesCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -148,6 +160,8 @@ class FlowUpdatesCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
|
||||
@@ -46,6 +46,8 @@ class TemplateValidateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
|
||||
@@ -31,6 +31,8 @@ class NamespaceFilesUpdateCommandTest {
|
||||
|
||||
String to = "/some/directory";
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -61,6 +63,8 @@ class NamespaceFilesUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -90,6 +94,8 @@ class NamespaceFilesUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
|
||||
@@ -28,6 +28,8 @@ class KvUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -54,6 +56,8 @@ class KvUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -80,6 +84,8 @@ class KvUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -108,6 +114,8 @@ class KvUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -134,6 +142,8 @@ class KvUpdateCommandTest {
|
||||
embeddedServer.start();
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
@@ -167,6 +177,8 @@ class KvUpdateCommandTest {
|
||||
Files.write(file.toPath(), "{\"some\":\"json\",\"from\":\"file\"}".getBytes());
|
||||
|
||||
String[] args = {
|
||||
"--plugins",
|
||||
"/tmp", // pass this arg because it can cause failure
|
||||
"--server",
|
||||
embeddedServer.getURL().toString(),
|
||||
"--user",
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package io.kestra.cli.commands.sys.statestore;
|
||||
|
||||
import com.devskiller.friendly_id.FriendlyId;
|
||||
import io.kestra.core.exceptions.MigrationRequiredException;
|
||||
import io.kestra.core.exceptions.ResourceExpiredException;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.runners.RunContextFactory;
|
||||
import io.kestra.core.storages.StateStore;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.utils.Hashing;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.Slugify;
|
||||
import io.kestra.plugin.core.log.Log;
|
||||
import io.micronaut.configuration.picocli.PicocliRunner;
|
||||
@@ -27,7 +26,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
|
||||
class StateStoreMigrateCommandTest {
|
||||
@@ -45,7 +43,7 @@ class StateStoreMigrateCommandTest {
|
||||
.namespace("some.valid.namespace." + ((int) (Math.random() * 1000000)))
|
||||
.tasks(List.of(Log.builder().id("log").type(Log.class.getName()).message("logging").build()))
|
||||
.build();
|
||||
flowRepository.create(flow, flow.generateSource(), flow);
|
||||
flowRepository.create(GenericFlow.of(flow));
|
||||
|
||||
StorageInterface storage = ctx.getBean(StorageInterface.class);
|
||||
String tenantId = flow.getTenantId();
|
||||
|
||||
@@ -10,6 +10,8 @@ import io.micronaut.context.env.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -27,6 +29,10 @@ public abstract class KestraContext {
|
||||
// Properties
|
||||
public static final String KESTRA_SERVER_TYPE = "kestra.server-type";
|
||||
|
||||
// Those properties are injected bases on the CLI args.
|
||||
private static final String KESTRA_WORKER_MAX_NUM_THREADS = "kestra.worker.max-num-threads";
|
||||
private static final String KESTRA_WORKER_GROUP_KEY = "kestra.worker.group-key";
|
||||
|
||||
/**
|
||||
* Gets the current {@link KestraContext}.
|
||||
*
|
||||
@@ -54,6 +60,12 @@ public abstract class KestraContext {
|
||||
*/
|
||||
public abstract ServerType getServerType();
|
||||
|
||||
public abstract Optional<Integer> getWorkerMaxNumThreads();
|
||||
|
||||
public abstract Optional<String> getWorkerGroupKey();
|
||||
|
||||
public abstract void injectWorkerConfigs(Integer maxNumThreads, String workerGroupKey);
|
||||
|
||||
/**
|
||||
* Returns the Kestra Version.
|
||||
*
|
||||
@@ -110,6 +122,34 @@ public abstract class KestraContext {
|
||||
.orElse(ServerType.STANDALONE);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public Optional<Integer> getWorkerMaxNumThreads() {
|
||||
return Optional.ofNullable(environment)
|
||||
.flatMap(env -> env.getProperty(KESTRA_WORKER_MAX_NUM_THREADS, Integer.class));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public Optional<String> getWorkerGroupKey() {
|
||||
return Optional.ofNullable(environment)
|
||||
.flatMap(env -> env.getProperty(KESTRA_WORKER_GROUP_KEY, String.class));
|
||||
}
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public void injectWorkerConfigs(Integer maxNumThreads, String workerGroupKey) {
|
||||
final Map<String, Object> configs = new HashMap<>();
|
||||
Optional.ofNullable(maxNumThreads)
|
||||
.ifPresent(val -> configs.put(KESTRA_WORKER_MAX_NUM_THREADS, val));
|
||||
|
||||
Optional.ofNullable(workerGroupKey)
|
||||
.ifPresent(val -> configs.put(KESTRA_WORKER_GROUP_KEY, val));
|
||||
|
||||
if (!configs.isEmpty()) {
|
||||
environment.addPropertySource("kestra-runtime", configs);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public void shutdown() {
|
||||
|
||||
@@ -64,13 +64,21 @@ public class JsonSchemaGenerator {
|
||||
return this.schemas(cls, false);
|
||||
}
|
||||
|
||||
private void replaceOneOfWithAnyOf(ObjectNode objectNode) {
|
||||
objectNode.findParents("oneOf").forEach(jsonNode -> {
|
||||
if (jsonNode instanceof ObjectNode oNode) {
|
||||
oNode.set("anyOf", oNode.remove("oneOf"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T> Map<String, Object> schemas(Class<? extends T> cls, boolean arrayOf) {
|
||||
SchemaGeneratorConfigBuilder builder = new SchemaGeneratorConfigBuilder(
|
||||
SchemaVersion.DRAFT_7,
|
||||
OptionPreset.PLAIN_JSON
|
||||
);
|
||||
|
||||
this.build(builder,true);
|
||||
this.build(builder, true);
|
||||
|
||||
SchemaGeneratorConfig schemaGeneratorConfig = builder.build();
|
||||
|
||||
@@ -80,8 +88,8 @@ public class JsonSchemaGenerator {
|
||||
if (arrayOf) {
|
||||
objectNode.put("type", "array");
|
||||
}
|
||||
replaceAnyOfWithOneOf(objectNode);
|
||||
pullOfDefaultFromOneOf(objectNode);
|
||||
replaceOneOfWithAnyOf(objectNode);
|
||||
pullDocumentationAndDefaultFromAnyOf(objectNode);
|
||||
removeRequiredOnPropsWithDefaults(objectNode);
|
||||
|
||||
return JacksonMapper.toMap(objectNode);
|
||||
@@ -111,33 +119,38 @@ public class JsonSchemaGenerator {
|
||||
});
|
||||
}
|
||||
|
||||
private void replaceAnyOfWithOneOf(ObjectNode objectNode) {
|
||||
// This hack exists because for Property we generate a anyOf for properties that are not strings.
|
||||
// By default, the 'default' is in each anyOf which Monaco editor didn't take into account.
|
||||
// So, we pull off the 'default' from any of the anyOf to the parent.
|
||||
// same thing for documentation fields: 'title', 'description', '$deprecated'
|
||||
private void pullDocumentationAndDefaultFromAnyOf(ObjectNode objectNode) {
|
||||
objectNode.findParents("anyOf").forEach(jsonNode -> {
|
||||
if (jsonNode instanceof ObjectNode oNode) {
|
||||
oNode.set("oneOf", oNode.remove("anyOf"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This hack exists because for Property we generate a oneOf for properties that are not strings.
|
||||
// By default, the 'default' is in each oneOf which Monaco editor didn't take into account.
|
||||
// So, we pull off the 'default' from any of the oneOf to the parent.
|
||||
private void pullOfDefaultFromOneOf(ObjectNode objectNode) {
|
||||
objectNode.findParents("oneOf").forEach(jsonNode -> {
|
||||
if (jsonNode instanceof ObjectNode oNode) {
|
||||
JsonNode oneOf = oNode.get("oneOf");
|
||||
if (oneOf instanceof ArrayNode arrayNode) {
|
||||
JsonNode anyOf = oNode.get("anyOf");
|
||||
if (anyOf instanceof ArrayNode arrayNode) {
|
||||
Iterator<JsonNode> it = arrayNode.elements();
|
||||
JsonNode defaultNode = null;
|
||||
while (it.hasNext() && defaultNode == null) {
|
||||
var nodesToPullUp = new HashMap<String, Optional<JsonNode>>(Map.ofEntries(
|
||||
Map.entry("default", Optional.empty()),
|
||||
Map.entry("title", Optional.empty()),
|
||||
Map.entry("description", Optional.empty()),
|
||||
Map.entry("$deprecated", Optional.empty())
|
||||
));
|
||||
// find nodes to pull up
|
||||
while (it.hasNext() && nodesToPullUp.containsValue(Optional.<JsonNode>empty())) {
|
||||
JsonNode next = it.next();
|
||||
if (next instanceof ObjectNode nextAsObj) {
|
||||
defaultNode = nextAsObj.get("default");
|
||||
nodesToPullUp.entrySet().stream()
|
||||
.filter(node -> node.getValue().isEmpty())
|
||||
.forEach(node -> node
|
||||
.setValue(Optional.ofNullable(
|
||||
nextAsObj.get(node.getKey())
|
||||
)));
|
||||
}
|
||||
}
|
||||
if (defaultNode != null) {
|
||||
oNode.set("default", defaultNode);
|
||||
}
|
||||
// create nodes on parent
|
||||
nodesToPullUp.entrySet().stream()
|
||||
.filter(node -> node.getValue().isPresent())
|
||||
.forEach(node -> oNode.set(node.getKey(), node.getValue().get()));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -274,11 +287,11 @@ public class JsonSchemaGenerator {
|
||||
TypeContext context = target.getContext();
|
||||
Class<?> erasedType = javaType.getTypeParameters().getFirst().getErasedType();
|
||||
|
||||
if(String.class.isAssignableFrom(erasedType)) {
|
||||
if (String.class.isAssignableFrom(erasedType)) {
|
||||
return List.of(
|
||||
context.resolve(String.class)
|
||||
);
|
||||
} else if(Object.class.equals(erasedType)) {
|
||||
} else if (Object.class.equals(erasedType)) {
|
||||
return List.of(
|
||||
context.resolve(Object.class)
|
||||
);
|
||||
@@ -388,7 +401,7 @@ public class JsonSchemaGenerator {
|
||||
// handle deprecated tasks
|
||||
Schema schema = scope.getType().getErasedType().getAnnotation(Schema.class);
|
||||
Deprecated deprecated = scope.getType().getErasedType().getAnnotation(Deprecated.class);
|
||||
if ((schema != null && schema.deprecated()) || deprecated != null ) {
|
||||
if ((schema != null && schema.deprecated()) || deprecated != null) {
|
||||
collectedTypeAttributes.put("$deprecated", "true");
|
||||
}
|
||||
});
|
||||
@@ -413,7 +426,7 @@ public class JsonSchemaGenerator {
|
||||
});
|
||||
|
||||
// Subtype resolver for all plugins
|
||||
if(builder.build().getSchemaVersion() != SchemaVersion.DRAFT_2019_09) {
|
||||
if (builder.build().getSchemaVersion() != SchemaVersion.DRAFT_2019_09) {
|
||||
builder.forTypesInGeneral()
|
||||
.withSubtypeResolver((declaredType, context) -> {
|
||||
TypeContext typeContext = context.getTypeContext();
|
||||
@@ -602,7 +615,7 @@ public class JsonSchemaGenerator {
|
||||
if (property.has("allOf")) {
|
||||
for (Iterator<JsonNode> it = property.get("allOf").elements(); it.hasNext(); ) {
|
||||
JsonNode child = it.next();
|
||||
if(child.has("default")) {
|
||||
if (child.has("default")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -616,7 +629,7 @@ public class JsonSchemaGenerator {
|
||||
OptionPreset.PLAIN_JSON
|
||||
);
|
||||
|
||||
this.build(builder,false);
|
||||
this.build(builder, false);
|
||||
|
||||
// we don't return base properties unless specified with @PluginProperty
|
||||
builder
|
||||
@@ -628,8 +641,8 @@ public class JsonSchemaGenerator {
|
||||
SchemaGenerator generator = new SchemaGenerator(schemaGeneratorConfig);
|
||||
try {
|
||||
ObjectNode objectNode = generator.generateSchema(cls);
|
||||
replaceAnyOfWithOneOf(objectNode);
|
||||
pullOfDefaultFromOneOf(objectNode);
|
||||
replaceOneOfWithAnyOf(objectNode);
|
||||
pullDocumentationAndDefaultFromAnyOf(objectNode);
|
||||
removeRequiredOnPropsWithDefaults(objectNode);
|
||||
|
||||
return JacksonMapper.toMap(extractMainRef(objectNode));
|
||||
@@ -740,7 +753,8 @@ public class JsonSchemaGenerator {
|
||||
|
||||
field.setAccessible(true);
|
||||
return field.invoke(instance);
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IllegalArgumentException ignored) {
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
|
||||
IllegalArgumentException ignored) {
|
||||
|
||||
}
|
||||
|
||||
@@ -749,7 +763,8 @@ public class JsonSchemaGenerator {
|
||||
|
||||
field.setAccessible(true);
|
||||
return field.invoke(instance);
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IllegalArgumentException ignored) {
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
|
||||
IllegalArgumentException ignored) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,10 @@ public class EncryptionService {
|
||||
* The IV is recovered from the beginning of the string.
|
||||
*
|
||||
* @see #decrypt(String, byte[])
|
||||
* @throws IllegalArgumentException when the cipherText cannot be BASE64 decoded.
|
||||
* This may indicate that the cipherText was not encrypted at first so a caller may use this as an indication as it tries to decode a text that was not encoded.
|
||||
*/
|
||||
public static String decrypt(String key, String cipherText) throws GeneralSecurityException {
|
||||
public static String decrypt(String key, String cipherText) throws GeneralSecurityException, IllegalArgumentException {
|
||||
if (cipherText == null || cipherText.isEmpty()) {
|
||||
return cipherText;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public enum CrudEventType {
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
IMPERSONATE,
|
||||
LOGIN_FAILURE
|
||||
LOGIN_FAILURE,
|
||||
ACCOUNT_LOCKED
|
||||
}
|
||||
|
||||
|
||||
@@ -23,4 +23,5 @@ public class KestraRuntimeException extends RuntimeException {
|
||||
public KestraRuntimeException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -157,25 +157,32 @@ public class HttpRequest {
|
||||
return null;
|
||||
}
|
||||
|
||||
Charset charset = entity.getContentEncoding() != null ? Charset.forName(entity.getContentEncoding()) : StandardCharsets.UTF_8;
|
||||
|
||||
if (entity.getContentType().equals(ContentType.APPLICATION_OCTET_STREAM.getMimeType())) {
|
||||
String[] parts = entity.getContentType().split(";");
|
||||
String mimeType = parts[0];
|
||||
Charset charset = StandardCharsets.UTF_8;
|
||||
for (String part : parts) {
|
||||
String stripped = part.strip();
|
||||
if (stripped.startsWith("charset")) {
|
||||
charset = Charset.forName(stripped.substring(stripped.lastIndexOf('=') + 1));
|
||||
}
|
||||
}
|
||||
if (mimeType.equals(ContentType.APPLICATION_OCTET_STREAM.getMimeType())) {
|
||||
return ByteArrayRequestBody.builder()
|
||||
.contentType(entity.getContentType())
|
||||
.contentType(mimeType)
|
||||
.charset(charset)
|
||||
.content(IOUtils.toByteArray(entity.getContent()))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (entity.getContentType().equals(ContentType.TEXT_PLAIN.getMimeType())) {
|
||||
if (mimeType.equals(ContentType.TEXT_PLAIN.getMimeType())) {
|
||||
return StringRequestBody.builder()
|
||||
.contentType(entity.getContentType())
|
||||
.contentType(mimeType)
|
||||
.charset(charset)
|
||||
.content(IOUtils.toString(entity.getContent(), charset))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (entity.getContentType().equals(ContentType.APPLICATION_JSON.getMimeType())) {
|
||||
if (mimeType.equals(ContentType.APPLICATION_JSON.getMimeType())) {
|
||||
return JsonRequestBody.builder()
|
||||
.charset(charset)
|
||||
.content(JacksonMapper.toObject(IOUtils.toString(entity.getContent(), charset)))
|
||||
@@ -184,7 +191,7 @@ public class HttpRequest {
|
||||
|
||||
return ByteArrayRequestBody.builder()
|
||||
.charset(charset)
|
||||
.contentType(entity.getContentType())
|
||||
.contentType(mimeType)
|
||||
.content(entity.getContent().readAllBytes())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public record Label(@NotNull String key, @NotNull String value) {
|
||||
public static final String RESTARTED = SYSTEM_PREFIX + "restarted";
|
||||
public static final String REPLAY = SYSTEM_PREFIX + "replay";
|
||||
public static final String REPLAYED = SYSTEM_PREFIX + "replayed";
|
||||
public static final String SIMULATED_EXECUTION = SYSTEM_PREFIX + "simulatedExecution";
|
||||
|
||||
/**
|
||||
* Static helper method for converting a list of labels to a nested map.
|
||||
|
||||
@@ -10,7 +10,7 @@ import jakarta.validation.constraints.Pattern;
|
||||
*/
|
||||
public interface PluginVersioning {
|
||||
|
||||
@Pattern(regexp="\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?|([a-zA-Z0-9]+)")
|
||||
@Pattern(regexp="\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?|([a-zA-Z0-9]+)")
|
||||
@Schema(title = "The version of the plugin to use.")
|
||||
String getVersion();
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public record QueryFilter(
|
||||
NAMESPACE("namespace") {
|
||||
@Override
|
||||
public List<Op> supportedOp() {
|
||||
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX);
|
||||
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX, Op.IN);
|
||||
}
|
||||
},
|
||||
LABELS("labels") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.core.models.conditions;
|
||||
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import lombok.*;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
@@ -18,7 +19,7 @@ import jakarta.validation.constraints.NotNull;
|
||||
@AllArgsConstructor
|
||||
public class ConditionContext {
|
||||
@NotNull
|
||||
private Flow flow;
|
||||
private FlowInterface flow;
|
||||
|
||||
private Execution execution;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.tasks.ResolvedTask;
|
||||
import io.kestra.core.runners.FlowableUtils;
|
||||
@@ -135,8 +136,8 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
* @param labels The Flow labels.
|
||||
* @return a new {@link Execution}.
|
||||
*/
|
||||
public static Execution newExecution(final Flow flow,
|
||||
final BiFunction<Flow, Execution, Map<String, Object>> inputs,
|
||||
public static Execution newExecution(final FlowInterface flow,
|
||||
final BiFunction<FlowInterface, Execution, Map<String, Object>> inputs,
|
||||
final List<Label> labels,
|
||||
final Optional<ZonedDateTime> scheduleDate) {
|
||||
Execution execution = builder()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelSerializer;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Builder;
|
||||
@@ -11,11 +15,13 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public abstract class AbstractFlow implements DeletedInterface, TenantInterface {
|
||||
@JsonDeserialize
|
||||
public abstract class AbstractFlow implements FlowInterface {
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9._-]*")
|
||||
@@ -33,6 +39,9 @@ public abstract class AbstractFlow implements DeletedInterface, TenantInterface
|
||||
@Valid
|
||||
List<Input<?>> inputs;
|
||||
|
||||
@Valid
|
||||
List<Output> outputs;
|
||||
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
boolean disabled = false;
|
||||
@@ -46,4 +55,11 @@ public abstract class AbstractFlow implements DeletedInterface, TenantInterface
|
||||
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
|
||||
String tenantId;
|
||||
|
||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
|
||||
@Schema(implementation = Object.class, oneOf = {List.class, Map.class})
|
||||
List<Label> labels;
|
||||
|
||||
Map<String, Object> variables;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,28 +6,20 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
|
||||
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.annotations.PluginProperty;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.sla.SLA;
|
||||
import io.kestra.core.models.listeners.Listener;
|
||||
import io.kestra.core.models.tasks.FlowableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelSerializer;
|
||||
import io.kestra.core.services.FlowService;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.validations.FlowValidation;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
@@ -38,11 +30,18 @@ import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A serializable flow with no source.
|
||||
* <p>
|
||||
* This class is planned for deprecation - use the {@link FlowWithSource}.
|
||||
*/
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@@ -67,11 +66,6 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
|
||||
String description;
|
||||
|
||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
|
||||
@Schema(implementation = Object.class, oneOf = {List.class, Map.class})
|
||||
List<Label> labels;
|
||||
|
||||
Map<String, Object> variables;
|
||||
|
||||
@Valid
|
||||
@@ -135,61 +129,6 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
@PluginProperty(beta = true)
|
||||
List<SLA> sla;
|
||||
|
||||
|
||||
/** {@inheritDoc **/
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public String uid() {
|
||||
return Flow.uid(this.getTenantId(), this.getNamespace(), this.getId(), Optional.ofNullable(this.revision));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String uidWithoutRevision() {
|
||||
return Flow.uidWithoutRevision(this.getTenantId(), this.getNamespace(), this.getId());
|
||||
}
|
||||
|
||||
public static String uid(Execution execution) {
|
||||
return IdUtils.fromParts(
|
||||
execution.getTenantId(),
|
||||
execution.getNamespace(),
|
||||
execution.getFlowId(),
|
||||
String.valueOf(execution.getFlowRevision())
|
||||
);
|
||||
}
|
||||
|
||||
public static String uid(String tenantId, String namespace, String id, Optional<Integer> revision) {
|
||||
return IdUtils.fromParts(
|
||||
tenantId,
|
||||
namespace,
|
||||
id,
|
||||
String.valueOf(revision.orElse(-1))
|
||||
);
|
||||
}
|
||||
|
||||
public static String uidWithoutRevision(String tenantId, String namespace, String id) {
|
||||
return IdUtils.fromParts(
|
||||
tenantId,
|
||||
namespace,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
public static String uid(Trigger trigger) {
|
||||
return IdUtils.fromParts(
|
||||
trigger.getTenantId(),
|
||||
trigger.getNamespace(),
|
||||
trigger.getFlowId()
|
||||
);
|
||||
}
|
||||
|
||||
public static String uidWithoutRevision(Execution execution) {
|
||||
return IdUtils.fromParts(
|
||||
execution.getTenantId(),
|
||||
execution.getNamespace(),
|
||||
execution.getFlowId()
|
||||
);
|
||||
}
|
||||
|
||||
public Stream<String> allTypes() {
|
||||
return Stream.of(
|
||||
Optional.ofNullable(triggers).orElse(Collections.emptyList()).stream().map(AbstractTrigger::getType),
|
||||
@@ -341,7 +280,7 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
);
|
||||
}
|
||||
|
||||
public boolean equalsWithoutRevision(Flow o) {
|
||||
public boolean equalsWithoutRevision(FlowInterface o) {
|
||||
try {
|
||||
return WITHOUT_REVISION_OBJECT_MAPPER.writeValueAsString(this).equals(WITHOUT_REVISION_OBJECT_MAPPER.writeValueAsString(o));
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -381,14 +320,6 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to generate the source of a flow.
|
||||
* Equivalent to <code>FlowService.generateSource(this);</code>
|
||||
*/
|
||||
public String generateSource() {
|
||||
return FlowService.generateSource(this);
|
||||
}
|
||||
|
||||
public Flow toDeleted() {
|
||||
return this.toBuilder()
|
||||
.revision(this.revision + 1)
|
||||
@@ -396,7 +327,13 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
.build();
|
||||
}
|
||||
|
||||
public FlowWithSource withSource(String source) {
|
||||
return FlowWithSource.of(this, source);
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* To be conservative a flow MUST not return any source.
|
||||
*/
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public String getSource() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.TaskForExecution;
|
||||
import io.kestra.core.models.triggers.AbstractTriggerForExecution;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
@@ -52,4 +52,10 @@ public class FlowForExecution extends AbstractFlow {
|
||||
.deleted(flow.isDeleted())
|
||||
.build();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public String getSource() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
71
core/src/main/java/io/kestra/core/models/flows/FlowId.java
Normal file
71
core/src/main/java/io/kestra/core/models/flows/FlowId.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a unique and global identifier for a flow.
|
||||
*/
|
||||
public interface FlowId {
|
||||
|
||||
String getId();
|
||||
|
||||
String getNamespace();
|
||||
|
||||
Integer getRevision();
|
||||
|
||||
String getTenantId();
|
||||
|
||||
|
||||
static String uid(FlowId flow) {
|
||||
return uid(flow.getTenantId(), flow.getNamespace(), flow.getId(), Optional.ofNullable(flow.getRevision()));
|
||||
}
|
||||
|
||||
static String uid(String tenantId, String namespace, String id, Optional<Integer> revision) {
|
||||
return of(tenantId, namespace, id, revision.orElse(-1)).toString();
|
||||
}
|
||||
|
||||
static String uidWithoutRevision(FlowId flow) {
|
||||
return of(flow.getTenantId(), flow.getNamespace(), flow.getId(), null).toString();
|
||||
}
|
||||
|
||||
static String uidWithoutRevision(String tenantId, String namespace, String id) {
|
||||
return of(tenantId, namespace, id,null).toString();
|
||||
}
|
||||
|
||||
static String uid(Trigger trigger) {
|
||||
return of(trigger.getTenantId(), trigger.getNamespace(), trigger.getFlowId(), null).toString();
|
||||
}
|
||||
|
||||
static String uidWithoutRevision(Execution execution) {
|
||||
return of(execution.getTenantId(), execution.getNamespace(), execution.getFlowId(), null).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method for constructing a new {@link FlowId}.
|
||||
*
|
||||
* @return a new {@link FlowId}.
|
||||
*/
|
||||
static FlowId of(String tenantId, String namespace, String id, Integer revision) {
|
||||
return new Default(tenantId, namespace, id, revision);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
class Default implements FlowId {
|
||||
private final String tenantId;
|
||||
private final String namespace;
|
||||
private final String id;
|
||||
private final Integer revision;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return IdUtils.fromParts(tenantId, namespace, id, Optional.ofNullable(revision).map(String::valueOf).orElse(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.HasSource;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.flows.sla.SLA;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The base interface for FLow.
|
||||
*/
|
||||
@JsonDeserialize(as = GenericFlow.class)
|
||||
public interface FlowInterface extends FlowId, DeletedInterface, TenantInterface, HasUID, HasSource {
|
||||
|
||||
Pattern YAML_REVISION_MATCHER = Pattern.compile("(?m)^revision: \\d+\n?");
|
||||
|
||||
boolean isDisabled();
|
||||
|
||||
boolean isDeleted();
|
||||
|
||||
List<Label> getLabels();
|
||||
|
||||
List<Input<?>> getInputs();
|
||||
|
||||
List<Output> getOutputs();
|
||||
|
||||
Map<String, Object> getVariables();
|
||||
|
||||
default Concurrency getConcurrency() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default List<SLA> getSla() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
String getSource();
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
default String source() {
|
||||
return getSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
default String uid() {
|
||||
return FlowId.uid(this);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
default String uidWithoutRevision() {
|
||||
return FlowId.uidWithoutRevision(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this flow is equals to the given flow.
|
||||
* <p>
|
||||
* This method is used to compare if two flow revisions are equal.
|
||||
*
|
||||
* @param flow The flow to compare.
|
||||
* @return {@code true} if both flows are the same. Otherwise {@code false}
|
||||
*/
|
||||
@JsonIgnore
|
||||
default boolean isSameWithSource(final FlowInterface flow) {
|
||||
return
|
||||
Objects.equals(this.uidWithoutRevision(), flow.uidWithoutRevision()) &&
|
||||
Objects.equals(this.isDeleted(), flow.isDeleted()) &&
|
||||
Objects.equals(this.isDisabled(), flow.isDisabled()) &&
|
||||
Objects.equals(sourceWithoutRevision(this.getSource()), sourceWithoutRevision(flow.getSource()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this flow matches the given {@link FlowId}.
|
||||
*
|
||||
* @param that The {@link FlowId}.
|
||||
* @return {@code true} if the passed id matches this flow.
|
||||
*/
|
||||
@JsonIgnore
|
||||
default boolean isSameId(FlowId that) {
|
||||
if (that == null) return false;
|
||||
return
|
||||
Objects.equals(this.getTenantId(), that.getTenantId()) &&
|
||||
Objects.equals(this.getNamespace(), that.getNamespace()) &&
|
||||
Objects.equals(this.getId(), that.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method for removing the 'revision' field from a flow.
|
||||
*
|
||||
* @param source The source.
|
||||
* @return The source without revision.
|
||||
*/
|
||||
static String sourceWithoutRevision(final String source) {
|
||||
return YAML_REVISION_MATCHER.matcher(source).replaceFirst("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source code for this flow or generate one if {@code null}.
|
||||
* <p>
|
||||
* This method must only be used for testing purpose or for handling backward-compatibility.
|
||||
*
|
||||
* @return the sourcecode.
|
||||
*/
|
||||
default String sourceOrGenerateIfNull() {
|
||||
return getSource() != null ? getSource() : SourceGenerator.generate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper class for generating source_code from a {@link FlowInterface} object.
|
||||
*
|
||||
* <p>
|
||||
* This class must only be used for testing purpose or for handling backward-compatibility.
|
||||
*/
|
||||
class SourceGenerator {
|
||||
private static final ObjectMapper NON_DEFAULT_OBJECT_MAPPER = JacksonMapper.ofJson()
|
||||
.copy()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
|
||||
|
||||
static String generate(final FlowInterface flow) {
|
||||
try {
|
||||
String json = NON_DEFAULT_OBJECT_MAPPER.writeValueAsString(flow);
|
||||
|
||||
Object map = SourceGenerator.fixSnakeYaml(JacksonMapper.toMap(json));
|
||||
|
||||
String source = JacksonMapper.ofYaml().writeValueAsString(map);
|
||||
|
||||
// remove the revision from the generated source
|
||||
return sourceWithoutRevision(source);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dirty hack but only concern previous flow with no source code in org.yaml.snakeyaml.emitter.Emitter:
|
||||
* <pre>
|
||||
* if (previousSpace) {
|
||||
* spaceBreak = true;
|
||||
* }
|
||||
* </pre>
|
||||
* This control will detect ` \n` as a no valid entry on a string and will break the multiline to transform in single line
|
||||
*
|
||||
* @param object the object to fix
|
||||
* @return the modified object
|
||||
*/
|
||||
private static Object fixSnakeYaml(Object object) {
|
||||
if (object instanceof Map<?, ?> mapValue) {
|
||||
return mapValue
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> new AbstractMap.SimpleEntry<>(
|
||||
fixSnakeYaml(entry.getKey()),
|
||||
fixSnakeYaml(entry.getValue())
|
||||
))
|
||||
.filter(entry -> entry.getValue() != null)
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
Map.Entry::getValue,
|
||||
(u, v) -> {
|
||||
throw new IllegalStateException(String.format("Duplicate key %s", u));
|
||||
},
|
||||
LinkedHashMap::new
|
||||
));
|
||||
} else if (object instanceof Collection<?> collectionValue) {
|
||||
return collectionValue
|
||||
.stream()
|
||||
.map(SourceGenerator::fixSnakeYaml)
|
||||
.toList();
|
||||
} else if (object instanceof String item) {
|
||||
if (item.contains("\n")) {
|
||||
return item.replaceAll("\\s+\\n", "\\\n");
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -21,11 +23,48 @@ import java.util.Optional;
|
||||
public class FlowWithException extends FlowWithSource {
|
||||
String exception;
|
||||
|
||||
public static FlowWithException from(final FlowInterface flow, final Exception exception) {
|
||||
return FlowWithException.builder()
|
||||
.id(flow.getId())
|
||||
.tenantId(flow.getTenantId())
|
||||
.namespace(flow.getNamespace())
|
||||
.revision(flow.getRevision())
|
||||
.deleted(flow.isDeleted())
|
||||
.exception(exception.getMessage())
|
||||
.tasks(List.of())
|
||||
.source(flow.getSource())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Optional<FlowWithException> from(final String source, final Exception exception, final Logger log) {
|
||||
log.error("Unable to deserialize a flow: {}", exception.getMessage());
|
||||
try {
|
||||
var jsonNode = JacksonMapper.ofJson().readTree(source);
|
||||
return FlowWithException.from(jsonNode, exception);
|
||||
} catch (IOException e) {
|
||||
// if we cannot create a FlowWithException, ignore the message
|
||||
log.error("Unexpected exception when trying to handle a deserialization error", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<FlowWithException> from(JsonNode jsonNode, Exception exception) {
|
||||
if (jsonNode.hasNonNull("id") && jsonNode.hasNonNull("namespace")) {
|
||||
|
||||
final String tenantId;
|
||||
if (jsonNode.hasNonNull("tenant_id")) {
|
||||
// JsonNode is from database
|
||||
tenantId = jsonNode.get("tenant_id").asText();
|
||||
} else if (jsonNode.hasNonNull("tenantId")) {
|
||||
// JsonNode is from queue
|
||||
tenantId = jsonNode.get("tenantId").asText();
|
||||
} else {
|
||||
tenantId = null;
|
||||
}
|
||||
|
||||
var flow = FlowWithException.builder()
|
||||
.id(jsonNode.get("id").asText())
|
||||
.tenantId(jsonNode.hasNonNull("tenant_id") ? jsonNode.get("tenant_id").asText() : null)
|
||||
.tenantId(tenantId)
|
||||
.namespace(jsonNode.get("namespace").asText())
|
||||
.revision(jsonNode.hasNonNull("revision") ? jsonNode.get("revision").asInt() : 1)
|
||||
.deleted(jsonNode.hasNonNull("deleted") && jsonNode.get("deleted").asBoolean())
|
||||
@@ -39,4 +78,10 @@ public class FlowWithException extends FlowWithSource {
|
||||
// if there is no id and namespace, we return null as we cannot create a meaningful FlowWithException
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public Flow toFlow() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,22 +18,14 @@ import lombok.experimental.SuperBuilder;
|
||||
@EqualsAndHashCode
|
||||
@FlowValidation
|
||||
public class FlowWithPath {
|
||||
private FlowWithSource flow;
|
||||
private FlowInterface flow;
|
||||
@Nullable
|
||||
private String tenantId;
|
||||
private String id;
|
||||
private String namespace;
|
||||
private String path;
|
||||
|
||||
public static FlowWithPath of(FlowWithSource flow, String path) {
|
||||
return FlowWithPath.builder()
|
||||
.id(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
.path(path)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static FlowWithPath of(Flow flow, String path) {
|
||||
public static FlowWithPath of(FlowInterface flow, String path) {
|
||||
return FlowWithPath.builder()
|
||||
.id(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import io.kestra.core.models.HasSource;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@Introspected
|
||||
@ToString
|
||||
public class FlowWithSource extends Flow implements HasSource {
|
||||
public class FlowWithSource extends Flow {
|
||||
|
||||
String source;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -42,15 +46,13 @@ public class FlowWithSource extends Flow implements HasSource {
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String cleanupSource(String source) {
|
||||
return source.replaceFirst("(?m)^revision: \\d+\n?","");
|
||||
}
|
||||
|
||||
public boolean equals(Flow flow, String flowSource) {
|
||||
return this.equalsWithoutRevision(flow) &&
|
||||
this.source.equals(cleanupSource(flowSource));
|
||||
@Override
|
||||
@JsonIgnore(value = false)
|
||||
public String getSource() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowWithSource toDeleted() {
|
||||
return this.toBuilder()
|
||||
.revision(this.revision + 1)
|
||||
@@ -85,10 +87,4 @@ public class FlowWithSource extends Flow implements HasSource {
|
||||
.sla(flow.sla)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public String source() {
|
||||
return getSource();
|
||||
}
|
||||
}
|
||||
|
||||
124
core/src/main/java/io/kestra/core/models/flows/GenericFlow.java
Normal file
124
core/src/main/java/io/kestra/core/models/flows/GenericFlow.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.kestra.core.exceptions.DeserializationException;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.flows.sla.SLA;
|
||||
import io.kestra.core.models.tasks.GenericTask;
|
||||
import io.kestra.core.models.triggers.GenericTrigger;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelSerializer;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents an un-typed {@link FlowInterface} implementation for which
|
||||
* most properties are backed by a {@link Map}.
|
||||
*
|
||||
* <p>
|
||||
* This implementation should be preferred over other implementations when
|
||||
* no direct access to tasks and triggers is required.
|
||||
*/
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonDeserialize
|
||||
public class GenericFlow extends AbstractFlow implements HasUID {
|
||||
|
||||
private String id;
|
||||
|
||||
private String namespace;
|
||||
|
||||
private Integer revision;
|
||||
|
||||
private List<Input<?>> inputs;
|
||||
|
||||
private Map<String, Object> variables;
|
||||
|
||||
@Builder.Default
|
||||
private boolean disabled = false;
|
||||
|
||||
@Builder.Default
|
||||
private boolean deleted = false;
|
||||
|
||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
|
||||
@Schema(implementation = Object.class, oneOf = {List.class, Map.class})
|
||||
private List<Label> labels;
|
||||
|
||||
private String tenantId;
|
||||
|
||||
private String source;
|
||||
|
||||
private List<SLA> sla;
|
||||
|
||||
private Concurrency concurrency;
|
||||
|
||||
private List<GenericTask> tasks;
|
||||
|
||||
private List<GenericTrigger> triggers;
|
||||
|
||||
@JsonIgnore
|
||||
@Builder.Default
|
||||
private Map<String, Object> additionalProperties = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Static helper method for constructing a {@link GenericFlow} from {@link FlowInterface}.
|
||||
*
|
||||
* @param flow The flow.
|
||||
* @return a new {@link GenericFlow}
|
||||
* @throws DeserializationException if source cannot be deserialized.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static GenericFlow of(final FlowInterface flow) throws DeserializationException {
|
||||
return fromYaml(flow.getTenantId(), flow.sourceOrGenerateIfNull());
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method for constructing a {@link GenericFlow} from a YAML source.
|
||||
*
|
||||
* @param source The flow YAML source.
|
||||
* @return a new {@link GenericFlow}
|
||||
* @throws DeserializationException if source cannot be deserialized.
|
||||
*/
|
||||
public static GenericFlow fromYaml(final String tenantId, final String source) throws DeserializationException {
|
||||
GenericFlow parsed = YamlParser.parse(source, GenericFlow.class);
|
||||
return parsed.toBuilder()
|
||||
.tenantId(tenantId)
|
||||
.source(source)
|
||||
.build();
|
||||
}
|
||||
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getAdditionalProperties() {
|
||||
return this.additionalProperties;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setAdditionalProperty(String name, Object value) {
|
||||
this.additionalProperties.put(name, value);
|
||||
}
|
||||
|
||||
public List<GenericTask> getTasks() {
|
||||
return Optional.ofNullable(tasks).orElse(List.of());
|
||||
}
|
||||
|
||||
public List<GenericTrigger> getTriggers() {
|
||||
return Optional.ofNullable(triggers).orElse(List.of());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.tasks.*;
|
||||
import io.kestra.core.runners.FlowExecutorInterface;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
@@ -52,7 +53,7 @@ public class SubflowGraphTask extends AbstractGraphTask {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SubflowExecutionResult> createSubflowExecutionResult(RunContext runContext, TaskRun taskRun, Flow flow, Execution execution) {
|
||||
public Optional<SubflowExecutionResult> createSubflowExecutionResult(RunContext runContext, TaskRun taskRun, FlowInterface flow, Execution execution) {
|
||||
return subflowTask.createSubflowExecutionResult(runContext, taskRun, flow, execution);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.runners.FlowExecutorInterface;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.runners.SubflowExecution;
|
||||
@@ -29,9 +31,9 @@ public interface ExecutableTask<T extends Output>{
|
||||
* Creates a SubflowExecutionResult for a given SubflowExecution
|
||||
*/
|
||||
Optional<SubflowExecutionResult> createSubflowExecutionResult(RunContext runContext,
|
||||
TaskRun taskRun,
|
||||
Flow flow,
|
||||
Execution execution);
|
||||
TaskRun taskRun,
|
||||
FlowInterface flow,
|
||||
Execution execution);
|
||||
|
||||
/**
|
||||
* Whether to wait for the execution(s) of the subflow before terminating this tasks
|
||||
@@ -51,12 +53,12 @@ public interface ExecutableTask<T extends Output>{
|
||||
record SubflowId(String namespace, String flowId, Optional<Integer> revision) {
|
||||
public String flowUid() {
|
||||
// as the Flow task can only be used in the same tenant we can hardcode null here
|
||||
return Flow.uid(null, this.namespace, this.flowId, this.revision);
|
||||
return FlowId.uid(null, this.namespace, this.flowId, this.revision);
|
||||
}
|
||||
|
||||
public String flowUidWithoutRevision() {
|
||||
// as the Flow task can only be used in the same tenant we can hardcode null here
|
||||
return Flow.uidWithoutRevision(null, this.namespace, this.flowId);
|
||||
return FlowId.uidWithoutRevision(null, this.namespace, this.flowId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.kestra.core.models.tasks;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonDeserialize
|
||||
public class GenericTask implements TaskInterface {
|
||||
|
||||
private String version;
|
||||
private String id;
|
||||
private String type;
|
||||
private WorkerGroup workerGroup;
|
||||
|
||||
@JsonIgnore
|
||||
@Builder.Default
|
||||
private Map<String, Object> additionalProperties = new HashMap<>();
|
||||
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getAdditionalProperties() {
|
||||
return this.additionalProperties;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setAdditionalProperty(String name, Object value) {
|
||||
this.additionalProperties.put(name, value);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,8 @@ public class NamespaceFiles {
|
||||
title = "A list of namespaces in which searching files. The files are loaded in the namespace order, and only the latest version of a file is kept. Meaning if a file is present in the first and second namespace, only the file present on the second namespace will be loaded."
|
||||
)
|
||||
@Builder.Default
|
||||
private Property<List<String>> namespaces = Property.of(List.of("{{flow.namespace}}"));
|
||||
private Property<List<String>> namespaces = new Property<>("""
|
||||
["{{flow.namespace}}"]""");
|
||||
|
||||
@Schema(
|
||||
title = "Comportment of the task if a file already exist in the working directory."
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.core.models.topologies;
|
||||
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
@@ -25,7 +26,7 @@ public class FlowNode implements TenantInterface {
|
||||
|
||||
String id;
|
||||
|
||||
public static FlowNode of(Flow flow) {
|
||||
public static FlowNode of(FlowInterface flow) {
|
||||
return FlowNode.builder()
|
||||
.uid(flow.uidWithoutRevision())
|
||||
.tenantId(flow.getTenantId())
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.kestra.core.models.triggers;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import io.kestra.core.models.tasks.WorkerGroup;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonDeserialize
|
||||
public class GenericTrigger implements TriggerInterface{
|
||||
|
||||
private String version;
|
||||
private String id;
|
||||
private String type;
|
||||
private WorkerGroup workerGroup;
|
||||
|
||||
@JsonIgnore
|
||||
@Builder.Default
|
||||
private Map<String, Object> additionalProperties = new HashMap<>();
|
||||
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getAdditionalProperties() {
|
||||
return this.additionalProperties;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setAdditionalProperty(String name, Object value) {
|
||||
this.additionalProperties.put(name, value);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.plugin.core.trigger.Schedule;
|
||||
@@ -81,13 +83,13 @@ public class Trigger extends TriggerContext implements HasUID {
|
||||
}
|
||||
|
||||
public String flowUid() {
|
||||
return Flow.uidWithoutRevision(this.getTenantId(), this.getNamespace(), this.getFlowId());
|
||||
return FlowId.uidWithoutRevision(this.getTenantId(), this.getNamespace(), this.getFlowId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Trigger with no execution information and no evaluation lock.
|
||||
*/
|
||||
public static Trigger of(Flow flow, AbstractTrigger abstractTrigger) {
|
||||
public static Trigger of(FlowInterface flow, AbstractTrigger abstractTrigger) {
|
||||
return Trigger.builder()
|
||||
.tenantId(flow.getTenantId())
|
||||
.namespace(flow.getNamespace())
|
||||
@@ -163,7 +165,7 @@ public class Trigger extends TriggerContext implements HasUID {
|
||||
}
|
||||
|
||||
// Used to update trigger in flowListeners
|
||||
public static Trigger of(Flow flow, AbstractTrigger abstractTrigger, ConditionContext conditionContext, Optional<Trigger> lastTrigger) throws Exception {
|
||||
public static Trigger of(FlowInterface flow, AbstractTrigger abstractTrigger, ConditionContext conditionContext, Optional<Trigger> lastTrigger) throws Exception {
|
||||
ZonedDateTime nextDate = null;
|
||||
|
||||
if (abstractTrigger instanceof PollingTriggerInterface pollingTriggerInterface) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.core.models.triggers.multipleflows;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.triggers.TimeWindow;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
@@ -15,11 +16,11 @@ import java.util.Optional;
|
||||
import static io.kestra.core.models.triggers.TimeWindow.Type.DURATION_WINDOW;
|
||||
|
||||
public interface MultipleConditionStorageInterface {
|
||||
Optional<MultipleConditionWindow> get(Flow flow, String conditionId);
|
||||
Optional<MultipleConditionWindow> get(FlowId flow, String conditionId);
|
||||
|
||||
List<MultipleConditionWindow> expired(String tenantId);
|
||||
|
||||
default MultipleConditionWindow getOrCreate(Flow flow, MultipleCondition multipleCondition, Map<String, Object> outputs) {
|
||||
default MultipleConditionWindow getOrCreate(FlowId flow, MultipleCondition multipleCondition, Map<String, Object> outputs) {
|
||||
ZonedDateTime now = ZonedDateTime.now().withNano(0);
|
||||
TimeWindow timeWindow = multipleCondition.getTimeWindow() != null ? multipleCondition.getTimeWindow() : TimeWindow.builder().build();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.kestra.core.models.triggers.multipleflows;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
@@ -44,7 +45,7 @@ public class MultipleConditionWindow implements HasUID {
|
||||
);
|
||||
}
|
||||
|
||||
public static String uid(Flow flow, String conditionId) {
|
||||
public static String uid(FlowId flow, String conditionId) {
|
||||
return IdUtils.fromParts(
|
||||
flow.getTenantId(),
|
||||
flow.getNamespace(),
|
||||
|
||||
@@ -38,7 +38,7 @@ public record PluginArtifact(
|
||||
"([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"
|
||||
);
|
||||
private static final Pattern FILENAME_PATTERN = Pattern.compile(
|
||||
"^(?<groupId>[\\w_]+)__(?<artifactId>[\\w-_]+)(?:__(?<classifier>[\\w-_]+))?__(?<version>\\d+_\\d+_\\d+(-[a-zA-Z0-9]+)?|([a-zA-Z0-9]+))\\.jar$"
|
||||
"^(?<groupId>[\\w_]+)__(?<artifactId>[\\w-_]+)(?:__(?<classifier>[\\w-_]+))?__(?<version>\\d+_\\d+_\\d+(-[a-zA-Z0-9-]+)?|([a-zA-Z0-9]+))\\.jar$"
|
||||
);
|
||||
|
||||
public static final String JAR_EXTENSION = "jar";
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionKilled;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.executions.MetricEntry;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.runners.*;
|
||||
@@ -42,7 +43,7 @@ public interface QueueFactoryInterface {
|
||||
|
||||
QueueInterface<MetricEntry> metricEntry();
|
||||
|
||||
QueueInterface<FlowWithSource> flow();
|
||||
QueueInterface<FlowInterface> flow();
|
||||
|
||||
QueueInterface<ExecutionKilled> kill();
|
||||
|
||||
|
||||
@@ -44,5 +44,9 @@ public interface QueueInterface<T> extends Closeable, Pauseable {
|
||||
return receive(consumerGroup, queueType, consumer, true);
|
||||
}
|
||||
|
||||
Runnable receive(String consumerGroup, Class<?> queueType, Consumer<Either<T, DeserializationException>> consumer, boolean forUpdate);
|
||||
default Runnable receive(String consumerGroup, Class<?> queueType, Consumer<Either<T, DeserializationException>> consumer, boolean forUpdate) {
|
||||
return receive(consumerGroup, queueType, consumer, forUpdate, false);
|
||||
}
|
||||
|
||||
Runnable receive(String consumerGroup, Class<?> queueType, Consumer<Either<T, DeserializationException>> consumer, boolean forUpdate, boolean delete);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
|
||||
boolean allowDeleted
|
||||
);
|
||||
|
||||
Flux<Execution> findAllAsync(@Nullable String tenantId);
|
||||
|
||||
ArrayListTotal<TaskRun> findTaskRun(
|
||||
Pageable pageable,
|
||||
|
||||
@@ -5,8 +5,10 @@ import io.kestra.core.models.SearchResult;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowForExecution;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowScope;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
@@ -176,9 +178,9 @@ public interface FlowRepositoryInterface {
|
||||
.toList();
|
||||
}
|
||||
|
||||
FlowWithSource create(Flow flow, String flowSource, Flow flowWithDefaults);
|
||||
FlowWithSource create(GenericFlow flow);
|
||||
|
||||
FlowWithSource update(Flow flow, Flow previous, String flowSource, Flow flowWithDefaults) throws ConstraintViolationException;
|
||||
FlowWithSource update(GenericFlow flow, FlowInterface previous) throws ConstraintViolationException;
|
||||
|
||||
FlowWithSource delete(FlowWithSource flow);
|
||||
FlowWithSource delete(FlowInterface flow);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package io.kestra.core.repositories;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import io.kestra.core.utils.Rethrow;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
@@ -15,22 +20,22 @@ import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwConsumer;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class LocalFlowRepositoryLoader {
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepository;
|
||||
@@ -68,47 +73,32 @@ public class LocalFlowRepositoryLoader {
|
||||
}
|
||||
|
||||
public void load(File basePath) throws IOException {
|
||||
Map<String, Flow> flowByUidInRepository = flowRepository.findAllForAllTenants().stream()
|
||||
.collect(Collectors.toMap(Flow::uidWithoutRevision, Function.identity()));
|
||||
List<Path> list = Files.walk(basePath.toPath())
|
||||
.filter(YamlParser::isValidExtension)
|
||||
.toList();
|
||||
Map<String, FlowInterface> flowByUidInRepository = flowRepository.findAllForAllTenants().stream()
|
||||
.collect(Collectors.toMap(FlowId::uidWithoutRevision, Function.identity()));
|
||||
|
||||
for (Path file : list) {
|
||||
try {
|
||||
String flowSource = Files.readString(Path.of(file.toFile().getPath()), Charset.defaultCharset());
|
||||
Flow parse = yamlParser.parse(file.toFile(), Flow.class);
|
||||
modelValidator.validate(parse);
|
||||
try (Stream<Path> pathStream = Files.walk(basePath.toPath())) {
|
||||
pathStream.filter(YamlParser::isValidExtension)
|
||||
.forEach(Rethrow.throwConsumer(file -> {
|
||||
try {
|
||||
String source = Files.readString(Path.of(file.toFile().getPath()), Charset.defaultCharset());
|
||||
GenericFlow parsed = GenericFlow.fromYaml(null, source);
|
||||
|
||||
Flow inRepository = flowByUidInRepository.get(parse.uidWithoutRevision());
|
||||
FlowWithSource flowWithSource = pluginDefaultService.injectAllDefaults(parsed, false);
|
||||
modelValidator.validate(flowWithSource);
|
||||
|
||||
if (inRepository == null) {
|
||||
this.createFlow(flowSource, parse);
|
||||
} else {
|
||||
this.udpateFlow(flowSource, parse, inRepository);
|
||||
}
|
||||
} catch (ConstraintViolationException e) {
|
||||
log.warn("Unable to create flow {}", file, e);
|
||||
}
|
||||
FlowInterface existing = flowByUidInRepository.get(flowWithSource.uidWithoutRevision());
|
||||
|
||||
if (existing == null) {
|
||||
flowRepository.create(parsed);
|
||||
log.trace("Created flow {}.{}", parsed.getNamespace(), parsed.getId());
|
||||
} else {
|
||||
flowRepository.update(parsed, existing);
|
||||
log.trace("Updated flow {}.{}", parsed.getNamespace(), parsed.getId());
|
||||
}
|
||||
} catch (ConstraintViolationException e) {
|
||||
log.warn("Unable to create flow {}", file, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void createFlow(String flowSource, Flow parse) {
|
||||
flowRepository.create(
|
||||
parse,
|
||||
flowSource,
|
||||
parse
|
||||
);
|
||||
log.trace("Created flow {}.{}", parse.getNamespace(), parse.getId());
|
||||
}
|
||||
|
||||
private void udpateFlow(String flowSource, Flow parse, Flow previous) {
|
||||
flowRepository.update(
|
||||
parse,
|
||||
previous,
|
||||
flowSource,
|
||||
parse
|
||||
);
|
||||
log.trace("Updated flow {}.{}", parse.getNamespace(), parse.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ public interface LogRepositoryInterface extends SaveRepositoryInterface<LogEntry
|
||||
ZonedDateTime startDate
|
||||
);
|
||||
|
||||
Flux<LogEntry> findAllAsync(@Nullable String tenantId);
|
||||
|
||||
List<LogStatistics> statistics(
|
||||
@Nullable String query,
|
||||
@Nullable String tenantId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.executions.metrics.MetricAggregations;
|
||||
import io.kestra.plugin.core.dashboard.data.Metrics;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
@@ -28,6 +29,8 @@ public interface MetricRepositoryInterface extends SaveRepositoryInterface<Metri
|
||||
|
||||
Integer purge(Execution execution);
|
||||
|
||||
Flux<MetricEntry> findAllAsync(@Nullable String tenantId);
|
||||
|
||||
default Function<String, String> sortMapping() throws IllegalArgumentException {
|
||||
return s -> s;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.core.runners;
|
||||
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.services.FlowListenersInterface;
|
||||
@@ -20,8 +21,7 @@ public class DefaultFlowExecutor implements FlowExecutorInterface {
|
||||
|
||||
public DefaultFlowExecutor(FlowListenersInterface flowListeners, FlowRepositoryInterface flowRepository) {
|
||||
this.flowRepository = flowRepository;
|
||||
|
||||
flowListeners.listen(flows -> this.allFlows = flows);
|
||||
flowListeners.listen(flows -> allFlows = flows);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,20 +30,22 @@ public class DefaultFlowExecutor implements FlowExecutorInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FlowWithSource> findById(String tenantId, String namespace, String id, Optional<Integer> revision) {
|
||||
Optional<FlowWithSource> find = this.allFlows
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public Optional<FlowInterface> findById(String tenantId, String namespace, String id, Optional<Integer> revision) {
|
||||
Optional<FlowInterface> find = this.allFlows
|
||||
.stream()
|
||||
.filter(flow -> ((flow.getTenantId() == null && tenantId == null) || Objects.equals(flow.getTenantId(), tenantId)) &&
|
||||
flow.getNamespace().equals(namespace) &&
|
||||
flow.getId().equals(id) &&
|
||||
(revision.isEmpty() || revision.get().equals(flow.getRevision()))
|
||||
)
|
||||
.map(it -> (FlowInterface)it)
|
||||
.findFirst();
|
||||
|
||||
if (find.isPresent()) {
|
||||
return find;
|
||||
} else {
|
||||
return flowRepository.findByIdWithSource(tenantId, namespace, id, revision);
|
||||
return (Optional) flowRepository.findByIdWithSource(tenantId, namespace, id, revision);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.*;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.property.Property;
|
||||
@@ -143,7 +144,7 @@ public final class ExecutableUtils {
|
||||
String subflowId = runContext.render(currentTask.subflowId().flowId());
|
||||
Optional<Integer> subflowRevision = currentTask.subflowId().revision();
|
||||
|
||||
Flow flow = flowExecutorInterface.findByIdFromTask(
|
||||
FlowInterface flow = flowExecutorInterface.findByIdFromTask(
|
||||
currentExecution.getTenantId(),
|
||||
subflowNamespace,
|
||||
subflowId,
|
||||
@@ -212,7 +213,7 @@ public final class ExecutableUtils {
|
||||
}));
|
||||
}
|
||||
|
||||
private static List<Label> filterLabels(List<Label> labels, Flow flow) {
|
||||
private static List<Label> filterLabels(List<Label> labels, FlowInterface flow) {
|
||||
if (ListUtils.isEmpty(flow.getLabels())) {
|
||||
return labels;
|
||||
}
|
||||
@@ -304,7 +305,7 @@ public final class ExecutableUtils {
|
||||
return State.Type.SUCCESS;
|
||||
}
|
||||
|
||||
public static SubflowExecutionResult subflowExecutionResultFromChildExecution(RunContext runContext, Flow flow, Execution execution, ExecutableTask<?> executableTask, TaskRun taskRun) {
|
||||
public static SubflowExecutionResult subflowExecutionResultFromChildExecution(RunContext runContext, FlowInterface flow, Execution execution, ExecutableTask<?> executableTask, TaskRun taskRun) {
|
||||
try {
|
||||
return executableTask
|
||||
.createSubflowExecutionResult(runContext, taskRun, flow, execution)
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.kestra.core.runners;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.kestra.core.models.executions.*;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.State;
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.metrics.MetricRegistry;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.*;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.flows.sla.Violation;
|
||||
@@ -92,7 +93,7 @@ public class ExecutorService {
|
||||
return this.flowExecutorInterface;
|
||||
}
|
||||
|
||||
public Executor checkConcurrencyLimit(Executor executor, Flow flow, Execution execution, long count) {
|
||||
public Executor checkConcurrencyLimit(Executor executor, FlowInterface flow, Execution execution, long count) {
|
||||
// if above the limit, handle concurrency limit based on its behavior
|
||||
if (count >= flow.getConcurrency().getLimit()) {
|
||||
return switch (flow.getConcurrency().getBehavior()) {
|
||||
@@ -902,7 +903,7 @@ public class ExecutorService {
|
||||
);
|
||||
} else {
|
||||
executions.addAll(subflowExecutions);
|
||||
Optional<FlowWithSource> flow = flowExecutorInterface.findByExecution(subflowExecutions.getFirst().getExecution());
|
||||
Optional<FlowInterface> flow = flowExecutorInterface.findByExecution(subflowExecutions.getFirst().getExecution());
|
||||
if (flow.isPresent()) {
|
||||
// add SubflowExecutionResults to notify parents
|
||||
for (SubflowExecution<?> subflowExecution : subflowExecutions) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.kestra.core.runners;
|
||||
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -18,7 +18,7 @@ public interface FlowExecutorInterface {
|
||||
* Find a flow.
|
||||
* WARNING: this method will NOT check if the namespace is allowed, so it should not be used inside a task.
|
||||
*/
|
||||
Optional<FlowWithSource> findById(String tenantId, String namespace, String id, Optional<Integer> revision);
|
||||
Optional<FlowInterface> findById(String tenantId, String namespace, String id, Optional<Integer> revision);
|
||||
|
||||
/**
|
||||
* Whether the FlowExecutorInterface is ready to be used.
|
||||
@@ -29,20 +29,15 @@ public interface FlowExecutorInterface {
|
||||
* Find a flow.
|
||||
* This method will check if the namespace is allowed, so it can be used inside a task.
|
||||
*/
|
||||
default Optional<FlowWithSource> findByIdFromTask(String tenantId, String namespace, String id, Optional<Integer> revision, String fromTenant, String fromNamespace, String fromId) {
|
||||
return this.findById(
|
||||
tenantId,
|
||||
namespace,
|
||||
id,
|
||||
revision
|
||||
);
|
||||
default Optional<FlowInterface> findByIdFromTask(String tenantId, String namespace, String id, Optional<Integer> revision, String fromTenant, String fromNamespace, String fromId) {
|
||||
return this.findById(tenantId, namespace, id, revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a flow from an execution.
|
||||
* WARNING: this method will NOT check if the namespace is allowed, so it should not be used inside a task.
|
||||
*/
|
||||
default Optional<FlowWithSource> findByExecution(Execution execution) {
|
||||
default Optional<FlowInterface> findByExecution(Execution execution) {
|
||||
if (execution.getFlowRevision() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Data;
|
||||
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.Input;
|
||||
import io.kestra.core.models.flows.RenderableInput;
|
||||
import io.kestra.core.models.flows.Type;
|
||||
@@ -110,7 +111,7 @@ public class FlowInputOutput {
|
||||
* @param data The Execution's inputs data.
|
||||
* @return The Map of typed inputs.
|
||||
*/
|
||||
public Mono<Map<String, Object>> readExecutionInputs(final Flow flow,
|
||||
public Mono<Map<String, Object>> readExecutionInputs(final FlowInterface flow,
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
return this.readExecutionInputs(flow.getInputs(), flow, execution, data);
|
||||
@@ -125,7 +126,7 @@ public class FlowInputOutput {
|
||||
* @return The Map of typed inputs.
|
||||
*/
|
||||
public Mono<Map<String, Object>> readExecutionInputs(final List<Input<?>> inputs,
|
||||
final Flow flow,
|
||||
final FlowInterface flow,
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, flow, execution, inputData));
|
||||
@@ -189,7 +190,7 @@ public class FlowInputOutput {
|
||||
* @return The Map of typed inputs.
|
||||
*/
|
||||
public Map<String, Object> readExecutionInputs(
|
||||
final Flow flow,
|
||||
final FlowInterface flow,
|
||||
final Execution execution,
|
||||
final Map<String, ?> data
|
||||
) {
|
||||
@@ -198,7 +199,7 @@ public class FlowInputOutput {
|
||||
|
||||
private Map<String, Object> readExecutionInputs(
|
||||
final List<Input<?>> inputs,
|
||||
final Flow flow,
|
||||
final FlowInterface flow,
|
||||
final Execution execution,
|
||||
final Map<String, ?> data
|
||||
) {
|
||||
@@ -227,7 +228,7 @@ public class FlowInputOutput {
|
||||
@VisibleForTesting
|
||||
public List<InputAndValue> resolveInputs(
|
||||
final List<Input<?>> inputs,
|
||||
final Flow flow,
|
||||
final FlowInterface flow,
|
||||
final Execution execution,
|
||||
final Map<String, ?> data
|
||||
) {
|
||||
@@ -251,7 +252,7 @@ public class FlowInputOutput {
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private InputAndValue resolveInputValue(
|
||||
final @NotNull ResolvableInput resolvable,
|
||||
final Flow flow,
|
||||
final FlowInterface flow,
|
||||
final @NotNull Execution execution,
|
||||
final @NotNull Map<String, ResolvableInput> inputs) {
|
||||
|
||||
@@ -329,7 +330,7 @@ public class FlowInputOutput {
|
||||
return resolvable.get();
|
||||
}
|
||||
|
||||
private RunContext buildRunContextForExecutionAndInputs(final Flow flow, final Execution execution, Map<String, InputAndValue> dependencies) {
|
||||
private RunContext buildRunContextForExecutionAndInputs(final FlowInterface flow, final Execution execution, Map<String, InputAndValue> dependencies) {
|
||||
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
|
||||
.stream()
|
||||
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue().value()), HashMap::putAll)
|
||||
@@ -337,7 +338,7 @@ public class FlowInputOutput {
|
||||
return runContextFactory.of(flow, execution, vars -> vars.withInputs(flattenInputs));
|
||||
}
|
||||
|
||||
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final Flow flow, final Execution execution, final Map<String, ResolvableInput> inputs) {
|
||||
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final FlowInterface flow, final Execution execution, final Map<String, ResolvableInput> inputs) {
|
||||
return Optional.ofNullable(input.getDependsOn())
|
||||
.map(DependsOn::inputs)
|
||||
.stream()
|
||||
@@ -350,7 +351,7 @@ public class FlowInputOutput {
|
||||
}
|
||||
|
||||
public Map<String, Object> typedOutputs(
|
||||
final Flow flow,
|
||||
final FlowInterface flow,
|
||||
final Execution execution,
|
||||
final Map<String, Object> in
|
||||
) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.kestra.core.runners;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import io.kestra.core.queues.QueueFactoryInterface;
|
||||
@@ -11,12 +11,9 @@ import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.services.FlowListenersInterface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
@@ -28,22 +25,24 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class FlowListeners implements FlowListenersInterface {
|
||||
private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
|
||||
|
||||
private final AtomicBoolean isStarted = new AtomicBoolean(false);
|
||||
private final QueueInterface<FlowWithSource> flowQueue;
|
||||
private final QueueInterface<FlowInterface> flowQueue;
|
||||
private final List<FlowWithSource> flows;
|
||||
private final List<Consumer<List<FlowWithSource>>> consumers = new CopyOnWriteArrayList<>();
|
||||
private final List<Consumer<List<FlowWithSource>>> consumers = new ArrayList<>();
|
||||
private final List<BiConsumer<FlowWithSource, FlowWithSource>> consumersEach = new ArrayList<>();
|
||||
|
||||
private final List<BiConsumer<FlowWithSource, FlowWithSource>> consumersEach = new CopyOnWriteArrayList<>();
|
||||
private final PluginDefaultService pluginDefaultService;
|
||||
|
||||
@Inject
|
||||
public FlowListeners(
|
||||
FlowRepositoryInterface flowRepository,
|
||||
@Named(QueueFactoryInterface.FLOW_NAMED) QueueInterface<FlowWithSource> flowQueue
|
||||
@Named(QueueFactoryInterface.FLOW_NAMED) QueueInterface<FlowInterface> flowQueue,
|
||||
PluginDefaultService pluginDefaultService
|
||||
) {
|
||||
this.flowQueue = flowQueue;
|
||||
this.flows = flowRepository.findAllWithSourceForAllTenants();
|
||||
this.flows = new ArrayList<>(flowRepository.findAllWithSourceForAllTenants());
|
||||
this.pluginDefaultService = pluginDefaultService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,19 +52,14 @@ public class FlowListeners implements FlowListenersInterface {
|
||||
this.flowQueue.receive(either -> {
|
||||
FlowWithSource flow;
|
||||
if (either.isRight()) {
|
||||
log.error("Unable to deserialize a flow: {}", either.getRight().getMessage());
|
||||
try {
|
||||
var jsonNode = MAPPER.readTree(either.getRight().getRecord());
|
||||
flow = FlowWithException.from(jsonNode, either.getRight()).orElseThrow(IOException::new);
|
||||
} catch (IOException e) {
|
||||
// if we cannot create a FlowWithException, ignore the message
|
||||
log.error("Unexpected exception when trying to handle a deserialization error", e);
|
||||
flow = FlowWithException.from(either.getRight().getRecord(), either.getRight(), log).orElse(null);
|
||||
if (flow == null) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
flow = pluginDefaultService.injectVersionDefaults(either.getLeft(), true);
|
||||
}
|
||||
else {
|
||||
flow = either.getLeft();
|
||||
}
|
||||
|
||||
Optional<FlowWithSource> previous = this.previous(flow);
|
||||
|
||||
if (flow.isDeleted()) {
|
||||
@@ -96,17 +90,14 @@ public class FlowListeners implements FlowListenersInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<FlowWithSource> previous(FlowWithSource flow) {
|
||||
private Optional<FlowWithSource> previous(final FlowWithSource flow) {
|
||||
List<FlowWithSource> copy = new ArrayList<>(flows);
|
||||
return copy
|
||||
.stream()
|
||||
.filter(r -> Objects.equals(r.getTenantId(), flow.getTenantId()) && r.getNamespace().equals(flow.getNamespace()) && r.getId().equals(flow.getId()))
|
||||
.findFirst();
|
||||
return copy.stream().filter(r -> r.isSameId(flow)).findFirst();
|
||||
}
|
||||
|
||||
private boolean remove(FlowWithSource flow) {
|
||||
private boolean remove(FlowInterface flow) {
|
||||
synchronized (this) {
|
||||
boolean remove = flows.removeIf(r -> Objects.equals(r.getTenantId(), flow.getTenantId()) && r.getNamespace().equals(flow.getNamespace()) && r.getId().equals(flow.getId()));
|
||||
boolean remove = flows.removeIf(r -> r.isSameId(flow));
|
||||
if (!remove && flow.isDeleted()) {
|
||||
log.warn("Can't remove flow {}.{}", flow.getNamespace(), flow.getId());
|
||||
}
|
||||
@@ -125,8 +116,7 @@ public class FlowListeners implements FlowListenersInterface {
|
||||
|
||||
private void notifyConsumers() {
|
||||
synchronized (this) {
|
||||
this.consumers
|
||||
.forEach(consumer -> consumer.accept(new ArrayList<>(this.flows)));
|
||||
this.consumers.forEach(consumer -> consumer.accept(new ArrayList<>(this.flows)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.metrics.MetricRegistry;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.Type;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
@@ -75,11 +76,11 @@ public class RunContextFactory {
|
||||
return applicationContext.getBean(RunContextInitializer.class);
|
||||
}
|
||||
|
||||
public RunContext of(Flow flow, Execution execution) {
|
||||
public RunContext of(FlowInterface flow, Execution execution) {
|
||||
return of(flow, execution, Function.identity());
|
||||
}
|
||||
|
||||
public RunContext of(Flow flow, Execution execution, Function<RunVariables.Builder, RunVariables.Builder> runVariableModifier) {
|
||||
public RunContext of(FlowInterface flow, Execution execution, Function<RunVariables.Builder, RunVariables.Builder> runVariableModifier) {
|
||||
RunContextLogger runContextLogger = runContextLoggerFactory.create(execution);
|
||||
|
||||
return newBuilder()
|
||||
@@ -100,11 +101,11 @@ public class RunContextFactory {
|
||||
.build();
|
||||
}
|
||||
|
||||
public RunContext of(Flow flow, Task task, Execution execution, TaskRun taskRun) {
|
||||
public RunContext of(FlowInterface flow, Task task, Execution execution, TaskRun taskRun) {
|
||||
return this.of(flow, task, execution, taskRun, true);
|
||||
}
|
||||
|
||||
public RunContext of(Flow flow, Task task, Execution execution, TaskRun taskRun, boolean decryptVariables) {
|
||||
public RunContext of(FlowInterface flow, Task task, Execution execution, TaskRun taskRun, boolean decryptVariables) {
|
||||
RunContextLogger runContextLogger = runContextLoggerFactory.create(taskRun, task);
|
||||
|
||||
return newBuilder()
|
||||
@@ -202,7 +203,7 @@ public class RunContextFactory {
|
||||
return of(Map.of());
|
||||
}
|
||||
|
||||
private List<String> secretInputsFromFlow(Flow flow) {
|
||||
private List<String> secretInputsFromFlow(FlowInterface flow) {
|
||||
if (flow == null || flow.getInputs() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -51,10 +51,12 @@ public class RunContextLogger implements Supplier<org.slf4j.Logger> {
|
||||
}
|
||||
|
||||
public RunContextLogger(QueueInterface<LogEntry> logQueue, LogEntry logEntry, org.slf4j.event.Level loglevel, boolean logToFile) {
|
||||
if (logEntry.getExecutionId() != null) {
|
||||
this.loggerName = "flow." + logEntry.getFlowId() + "." + logEntry.getExecutionId() + (logEntry.getTaskRunId() != null ? "." + logEntry.getTaskRunId() : "");
|
||||
} else {
|
||||
if (logEntry.getTaskId() != null) {
|
||||
this.loggerName = "flow." + logEntry.getFlowId() + "." + logEntry.getTaskId();
|
||||
} else if (logEntry.getTriggerId() != null) {
|
||||
this.loggerName = "flow." + logEntry.getFlowId() + "." + logEntry.getTriggerId();
|
||||
} else {
|
||||
this.loggerName = "flow." + logEntry.getFlowId();
|
||||
}
|
||||
|
||||
this.logQueue = logQueue;
|
||||
@@ -258,7 +260,8 @@ public class RunContextLogger implements Supplier<org.slf4j.Logger> {
|
||||
} else if (object instanceof String string) {
|
||||
return replaceSecret(string);
|
||||
} else {
|
||||
return object;
|
||||
// toString will be called anyway at some point so better to all it now
|
||||
return replaceSecret(object.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
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;
|
||||
@@ -73,7 +74,7 @@ public final class RunVariables {
|
||||
* @param flow The flow from which to create variables.
|
||||
* @return a new immutable {@link Map}.
|
||||
*/
|
||||
static Map<String, Object> of(final Flow flow) {
|
||||
static Map<String, Object> of(final FlowInterface flow) {
|
||||
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
|
||||
builder.put("id", flow.getId())
|
||||
.put("namespace", flow.getNamespace());
|
||||
@@ -105,7 +106,7 @@ public final class RunVariables {
|
||||
*/
|
||||
public interface Builder {
|
||||
|
||||
Builder withFlow(Flow flow);
|
||||
Builder withFlow(FlowInterface flow);
|
||||
|
||||
Builder withInputs(Map<String, Object> inputs);
|
||||
|
||||
@@ -147,7 +148,7 @@ public final class RunVariables {
|
||||
@With
|
||||
public static class DefaultBuilder implements RunVariables.Builder {
|
||||
|
||||
protected Flow flow;
|
||||
protected FlowInterface flow;
|
||||
protected Task task;
|
||||
protected Execution execution;
|
||||
protected TaskRun taskRun;
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.queues.QueueFactoryInterface;
|
||||
import io.kestra.core.queues.QueueInterface;
|
||||
@@ -47,7 +48,7 @@ public class RunnerUtils {
|
||||
return this.runOne(tenantId, namespace, flowId, revision, null, null, null);
|
||||
}
|
||||
|
||||
public Execution runOne(String tenantId, String namespace, String flowId, Integer revision, BiFunction<Flow, Execution, Map<String, Object>> inputs) throws TimeoutException, QueueException {
|
||||
public Execution runOne(String tenantId, String namespace, String flowId, Integer revision, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs) throws TimeoutException, QueueException {
|
||||
return this.runOne(tenantId, namespace, flowId, revision, inputs, null, null);
|
||||
}
|
||||
|
||||
@@ -55,11 +56,11 @@ public class RunnerUtils {
|
||||
return this.runOne(tenantId, namespace, flowId, null, null, duration, null);
|
||||
}
|
||||
|
||||
public Execution runOne(String tenantId, String namespace, String flowId, Integer revision, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
public Execution runOne(String tenantId, String namespace, String flowId, Integer revision, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
return this.runOne(tenantId, namespace, flowId, revision, inputs, duration, null);
|
||||
}
|
||||
|
||||
public Execution runOne(String tenantId, String namespace, String flowId, Integer revision, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration, List<Label> labels) throws TimeoutException, QueueException {
|
||||
public Execution runOne(String tenantId, String namespace, String flowId, Integer revision, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration, List<Label> labels) throws TimeoutException, QueueException {
|
||||
return this.runOne(
|
||||
flowRepository
|
||||
.findById(tenantId, namespace, flowId, revision != null ? Optional.of(revision) : Optional.empty())
|
||||
@@ -69,15 +70,15 @@ public class RunnerUtils {
|
||||
labels);
|
||||
}
|
||||
|
||||
public Execution runOne(Flow flow, BiFunction<Flow, Execution, Map<String, Object>> inputs) throws TimeoutException, QueueException {
|
||||
public Execution runOne(Flow flow, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs) throws TimeoutException, QueueException {
|
||||
return this.runOne(flow, inputs, null, null);
|
||||
}
|
||||
|
||||
public Execution runOne(Flow flow, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
public Execution runOne(Flow flow, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
return this.runOne(flow, inputs, duration, null);
|
||||
}
|
||||
|
||||
public Execution runOne(Flow flow, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration, List<Label> labels) throws TimeoutException, QueueException {
|
||||
public Execution runOne(Flow flow, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration, List<Label> labels) throws TimeoutException, QueueException {
|
||||
if (duration == null) {
|
||||
duration = Duration.ofSeconds(15);
|
||||
}
|
||||
@@ -93,7 +94,7 @@ public class RunnerUtils {
|
||||
return this.runOneUntilPaused(tenantId, namespace, flowId, null, null, null);
|
||||
}
|
||||
|
||||
public Execution runOneUntilPaused(String tenantId, String namespace, String flowId, Integer revision, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
public Execution runOneUntilPaused(String tenantId, String namespace, String flowId, Integer revision, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
return this.runOneUntilPaused(
|
||||
flowRepository
|
||||
.findById(tenantId, namespace, flowId, revision != null ? Optional.of(revision) : Optional.empty())
|
||||
@@ -103,7 +104,7 @@ public class RunnerUtils {
|
||||
);
|
||||
}
|
||||
|
||||
public Execution runOneUntilPaused(Flow flow, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
public Execution runOneUntilPaused(Flow flow, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
if (duration == null) {
|
||||
duration = DEFAULT_MAX_WAIT_DURATION;
|
||||
}
|
||||
@@ -119,7 +120,7 @@ public class RunnerUtils {
|
||||
return this.runOneUntilRunning(tenantId, namespace, flowId, null, null, null);
|
||||
}
|
||||
|
||||
public Execution runOneUntilRunning(String tenantId, String namespace, String flowId, Integer revision, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
public Execution runOneUntilRunning(String tenantId, String namespace, String flowId, Integer revision, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
return this.runOneUntilRunning(
|
||||
flowRepository
|
||||
.findById(tenantId, namespace, flowId, revision != null ? Optional.of(revision) : Optional.empty())
|
||||
@@ -129,7 +130,7 @@ public class RunnerUtils {
|
||||
);
|
||||
}
|
||||
|
||||
public Execution runOneUntilRunning(Flow flow, BiFunction<Flow, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
public Execution runOneUntilRunning(Flow flow, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration) throws TimeoutException, QueueException {
|
||||
if (duration == null) {
|
||||
duration = DEFAULT_MAX_WAIT_DURATION;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.kestra.core.runners;
|
||||
|
||||
import io.kestra.core.encryption.EncryptionService;
|
||||
import io.kestra.core.models.tasks.common.EncryptedString;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
@@ -50,8 +49,11 @@ final class Secret {
|
||||
try {
|
||||
String decoded = decrypt((String) map.get("value"));
|
||||
decryptedMap.put(entry.getKey(), decoded);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (GeneralSecurityException | IllegalArgumentException e) {
|
||||
// NOTE: in rare cases, if for ex a Worker didn't have the encryption but an Executor has it,
|
||||
// we can have a non-encrypted output that we try to decrypt, this will lead to an IllegalArgumentException.
|
||||
// As it could break the executor, the best is to do nothing in this case and only log an error.
|
||||
logger.get().warn("Unable to decrypt the output", e);
|
||||
}
|
||||
} else {
|
||||
decryptedMap.put(entry.getKey(), decrypt((Map<String, Object>) map));
|
||||
|
||||
@@ -13,6 +13,8 @@ import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionKilled;
|
||||
import io.kestra.core.models.executions.ExecutionKilledTrigger;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.State;
|
||||
@@ -32,7 +34,6 @@ import io.kestra.core.utils.Await;
|
||||
import io.kestra.core.utils.Either;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.context.event.ApplicationEventPublisher;
|
||||
import io.micronaut.inject.qualifiers.Qualifiers;
|
||||
@@ -172,6 +173,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
|
||||
// remove trigger on flow update, update local triggers store, and stop the trigger on the worker
|
||||
this.flowListeners.listen((flow, previous) -> {
|
||||
|
||||
if (flow.isDeleted() || previous != null) {
|
||||
List<AbstractTrigger> triggersDeleted = flow.isDeleted() ?
|
||||
ListUtils.emptyOnNull(flow.getTriggers()) :
|
||||
@@ -287,7 +289,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
|
||||
flows
|
||||
.stream()
|
||||
.map(flow -> pluginDefaultService.injectDefaults(flow, log))
|
||||
.map(flow -> pluginDefaultService.injectAllDefaults(flow, log))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(flow -> flow.getTriggers() != null && !flow.getTriggers().isEmpty())
|
||||
.flatMap(flow -> flow.getTriggers().stream().filter(trigger -> trigger instanceof WorkerTriggerInterface).map(trigger -> new FlowAndTrigger(flow, trigger)))
|
||||
@@ -314,7 +316,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
FlowWithWorkerTrigger flowWithWorkerTrigger = FlowWithWorkerTrigger.builder()
|
||||
.flow(flowAndTrigger.flow())
|
||||
.abstractTrigger(flowAndTrigger.trigger())
|
||||
.workerTrigger((WorkerTriggerInterface) flowAndTrigger.trigger())
|
||||
.conditionContext(conditionContext)
|
||||
.triggerContext(newTrigger)
|
||||
.build();
|
||||
@@ -346,7 +347,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
FlowWithWorkerTrigger flowWithWorkerTrigger = FlowWithWorkerTrigger.builder()
|
||||
.flow(flowAndTrigger.flow())
|
||||
.abstractTrigger(flowAndTrigger.trigger())
|
||||
.workerTrigger((WorkerTriggerInterface) flowAndTrigger.trigger())
|
||||
.conditionContext(conditionContext)
|
||||
.triggerContext(lastUpdate)
|
||||
.build();
|
||||
@@ -432,7 +432,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
List<String> flowToKeep = triggerContextsToEvaluate.stream().map(Trigger::getFlowId).toList();
|
||||
|
||||
triggerContextsToEvaluate.stream()
|
||||
.filter(trigger -> !flows.stream().map(FlowWithSource::uidWithoutRevision).toList().contains(Flow.uid(trigger)))
|
||||
.filter(trigger -> !flows.stream().map(FlowId::uidWithoutRevision).toList().contains(FlowId.uid(trigger)))
|
||||
.forEach(trigger -> {
|
||||
try {
|
||||
this.triggerState.delete(trigger);
|
||||
@@ -443,8 +443,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
|
||||
return flows
|
||||
.stream()
|
||||
.map(flow -> pluginDefaultService.injectDefaults(flow, log))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(flow -> flowToKeep.contains(flow.getId()))
|
||||
.filter(flow -> flow.getTriggers() != null && !flow.getTriggers().isEmpty())
|
||||
.filter(flow -> !flow.isDisabled() && !(flow instanceof FlowWithException))
|
||||
@@ -481,7 +479,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
flow,
|
||||
abstractTrigger,
|
||||
triggerContext,
|
||||
runContext,
|
||||
conditionContext.withVariables(
|
||||
ImmutableMap.of("trigger",
|
||||
ImmutableMap.of("date", triggerContext.getNextExecutionDate() != null ?
|
||||
@@ -496,9 +493,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
abstract public void handleNext(List<FlowWithSource> flows, ZonedDateTime now, BiConsumer<List<Trigger>, ScheduleContextInterface> consumer);
|
||||
|
||||
public List<FlowWithTriggers> schedulerTriggers() {
|
||||
Map<String, FlowWithSource> flows = this.flowListeners.flows()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(FlowWithSource::uidWithoutRevision, Function.identity()));
|
||||
Map<String, FlowWithSource> flows = getFlowsWithDefaults().stream()
|
||||
.collect(Collectors.toMap(FlowInterface::uidWithoutRevision, Function.identity()));
|
||||
|
||||
return this.triggerState.findAllForAllTenants().stream()
|
||||
.filter(trigger -> flows.containsKey(trigger.flowUid()))
|
||||
@@ -507,7 +503,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
flows.get(trigger.flowUid()),
|
||||
ListUtils.emptyOnNull(flows.get(trigger.flowUid()).getTriggers()).stream().filter(t -> t.getId().equals(trigger.getTriggerId())).findFirst().orElse(null),
|
||||
trigger,
|
||||
null,
|
||||
null
|
||||
)
|
||||
).toList();
|
||||
@@ -525,7 +520,9 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
|
||||
ZonedDateTime now = now();
|
||||
|
||||
this.handleNext(this.flowListeners.flows(), now, (triggers, scheduleContext) -> {
|
||||
final List<FlowWithSource> flowWithDefaults = getFlowsWithDefaults();
|
||||
|
||||
this.handleNext(flowWithDefaults, now, (triggers, scheduleContext) -> {
|
||||
if (triggers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -534,7 +531,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
.filter(trigger -> Boolean.FALSE.equals(trigger.getDisabled()))
|
||||
.toList();
|
||||
|
||||
List<FlowWithTriggers> schedulable = this.computeSchedulable(flowListeners.flows(), triggerContextsToEvaluate, scheduleContext);
|
||||
List<FlowWithTriggers> schedulable = this.computeSchedulable(flowWithDefaults, triggerContextsToEvaluate, scheduleContext);
|
||||
|
||||
metricRegistry
|
||||
.counter(MetricRegistry.SCHEDULER_LOOP_COUNT)
|
||||
@@ -555,7 +552,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
.map(flowWithTriggers -> FlowWithWorkerTrigger.builder()
|
||||
.flow(flowWithTriggers.getFlow())
|
||||
.abstractTrigger(flowWithTriggers.getAbstractTrigger())
|
||||
.workerTrigger((WorkerTriggerInterface) flowWithTriggers.getAbstractTrigger())
|
||||
.conditionContext(flowWithTriggers.getConditionContext())
|
||||
.triggerContext(flowWithTriggers.triggerContext
|
||||
.toBuilder()
|
||||
@@ -611,7 +607,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
e
|
||||
);
|
||||
}
|
||||
} else if (f.getWorkerTrigger() instanceof Schedulable schedule) {
|
||||
} else if (f.getAbstractTrigger() instanceof Schedulable schedule) {
|
||||
// This is the Schedule, all other triggers should have an interval.
|
||||
// So we evaluate it now as there is no need to send it to the worker.
|
||||
// Schedule didn't use the triggerState to allow backfill.
|
||||
@@ -666,6 +662,13 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
});
|
||||
}
|
||||
|
||||
private List<FlowWithSource> getFlowsWithDefaults() {
|
||||
return this.flowListeners.flows().stream()
|
||||
.map(flow -> pluginDefaultService.injectAllDefaults(flow, log))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void handleEvaluateWorkerTriggerResult(SchedulerExecutionWithTrigger result, ZonedDateTime
|
||||
nextExecutionDate) {
|
||||
Optional.ofNullable(result)
|
||||
@@ -820,35 +823,31 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
|
||||
private Optional<SchedulerExecutionWithTrigger> evaluateScheduleTrigger(FlowWithWorkerTrigger flowWithTrigger) {
|
||||
try {
|
||||
FlowWithWorkerTrigger flowWithWorkerTrigger = flowWithTrigger.from(pluginDefaultService.injectDefaults(
|
||||
flowWithTrigger.getFlow(),
|
||||
flowWithTrigger.getConditionContext().getRunContext().logger()
|
||||
));
|
||||
|
||||
// mutability dirty hack that forces the creation of a new triggerExecutionId
|
||||
DefaultRunContext runContext = (DefaultRunContext) flowWithWorkerTrigger.getConditionContext().getRunContext();
|
||||
DefaultRunContext runContext = (DefaultRunContext) flowWithTrigger.getConditionContext().getRunContext();
|
||||
runContextInitializer.forScheduler(
|
||||
runContext,
|
||||
flowWithWorkerTrigger.getTriggerContext(),
|
||||
flowWithWorkerTrigger.getAbstractTrigger()
|
||||
flowWithTrigger.getTriggerContext(),
|
||||
flowWithTrigger.getAbstractTrigger()
|
||||
);
|
||||
|
||||
Optional<Execution> evaluate = ((Schedulable) flowWithWorkerTrigger.getWorkerTrigger()).evaluate(
|
||||
flowWithWorkerTrigger.getConditionContext(),
|
||||
flowWithWorkerTrigger.getTriggerContext()
|
||||
Optional<Execution> evaluate = ((Schedulable) flowWithTrigger.getAbstractTrigger()).evaluate(
|
||||
flowWithTrigger.getConditionContext(),
|
||||
flowWithTrigger.getTriggerContext()
|
||||
);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
logService.logTrigger(
|
||||
flowWithWorkerTrigger.getTriggerContext(),
|
||||
flowWithTrigger.getTriggerContext(),
|
||||
Level.DEBUG,
|
||||
"[type: {}] {}",
|
||||
flowWithWorkerTrigger.getAbstractTrigger().getType(),
|
||||
flowWithTrigger.getAbstractTrigger().getType(),
|
||||
evaluate.map(execution -> "New execution '" + execution.getId() + "'").orElse("Empty evaluation")
|
||||
);
|
||||
}
|
||||
|
||||
flowWithWorkerTrigger.getConditionContext().getRunContext().cleanup();
|
||||
flowWithTrigger.getConditionContext().getRunContext().cleanup();
|
||||
|
||||
return evaluate.map(execution -> new SchedulerExecutionWithTrigger(
|
||||
execution,
|
||||
@@ -895,11 +894,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
}
|
||||
|
||||
private void sendWorkerTriggerToWorker(FlowWithWorkerTrigger flowWithTrigger) throws InternalException {
|
||||
FlowWithWorkerTrigger flowWithTriggerWithDefault = flowWithTrigger.from(
|
||||
pluginDefaultService.injectDefaults(flowWithTrigger.getFlow(),
|
||||
flowWithTrigger.getConditionContext().getRunContext().logger())
|
||||
);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
logService.logTrigger(
|
||||
flowWithTrigger.getTriggerContext(),
|
||||
@@ -911,23 +905,23 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
|
||||
var workerTrigger = WorkerTrigger
|
||||
.builder()
|
||||
.trigger(flowWithTriggerWithDefault.abstractTrigger)
|
||||
.triggerContext(flowWithTriggerWithDefault.triggerContext)
|
||||
.conditionContext(flowWithTriggerWithDefault.conditionContext)
|
||||
.trigger(flowWithTrigger.abstractTrigger)
|
||||
.triggerContext(flowWithTrigger.triggerContext)
|
||||
.conditionContext(flowWithTrigger.conditionContext)
|
||||
.build();
|
||||
try {
|
||||
Optional<WorkerGroup> workerGroup = workerGroupService.resolveGroupFromJob(workerTrigger);
|
||||
if (workerGroup.isPresent()) {
|
||||
// Check if the worker group exist
|
||||
String tenantId = flowWithTrigger.getFlow().getTenantId();
|
||||
RunContext runContext = flowWithTriggerWithDefault.conditionContext.getRunContext();
|
||||
RunContext runContext = flowWithTrigger.conditionContext.getRunContext();
|
||||
String workerGroupKey = runContext.render(workerGroup.get().getKey());
|
||||
if (workerGroupExecutorInterface.isWorkerGroupExistForKey(workerGroupKey, tenantId)) {
|
||||
// Check whether at-least one worker is available
|
||||
if (workerGroupExecutorInterface.isWorkerGroupAvailableForKey(workerGroupKey)) {
|
||||
this.workerJobQueue.emit(workerGroupKey, workerTrigger);
|
||||
} else {
|
||||
WorkerGroup.Fallback fallback = workerGroup.map(wg -> wg.getFallback()).orElse(WorkerGroup.Fallback.WAIT);
|
||||
WorkerGroup.Fallback fallback = workerGroup.map(WorkerGroup::getFallback).orElse(WorkerGroup.Fallback.WAIT);
|
||||
switch(fallback) {
|
||||
case FAIL -> runContext.logger()
|
||||
.error("No workers are available for worker group '{}', ignoring the trigger.", workerGroupKey);
|
||||
@@ -990,7 +984,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
private static class FlowWithWorkerTrigger {
|
||||
private FlowWithSource flow;
|
||||
private AbstractTrigger abstractTrigger;
|
||||
private WorkerTriggerInterface workerTrigger;
|
||||
private Trigger triggerContext;
|
||||
private ConditionContext conditionContext;
|
||||
|
||||
@@ -1004,7 +997,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
return this.toBuilder()
|
||||
.flow(flow)
|
||||
.abstractTrigger(abstractTrigger)
|
||||
.workerTrigger((WorkerTriggerInterface) abstractTrigger)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1019,7 +1011,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
return FlowWithWorkerTriggerNextDate.builder()
|
||||
.flow(f.getFlow())
|
||||
.abstractTrigger(f.getAbstractTrigger())
|
||||
.workerTrigger(f.getWorkerTrigger())
|
||||
.conditionContext(f.getConditionContext())
|
||||
.triggerContext(Trigger.builder()
|
||||
.tenantId(f.getTriggerContext().getTenantId())
|
||||
@@ -1044,7 +1035,6 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
private final FlowWithSource flow;
|
||||
private final AbstractTrigger abstractTrigger;
|
||||
private final Trigger triggerContext;
|
||||
private final RunContext runContext;
|
||||
private final ConditionContext conditionContext;
|
||||
|
||||
public String uid() {
|
||||
|
||||
@@ -201,7 +201,7 @@ public final class FileSerde {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> SequenceWriter createSequenceWriter(ObjectMapper objectMapper, Writer writer, TypeReference<T> type) throws IOException {
|
||||
public static <T> SequenceWriter createSequenceWriter(ObjectMapper objectMapper, Writer writer, TypeReference<T> type) throws IOException {
|
||||
return objectMapper.writerFor(type).writeValues(writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
||||
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
@@ -20,8 +19,7 @@ import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Singleton
|
||||
public class YamlParser {
|
||||
public final class YamlParser {
|
||||
private static final ObjectMapper STRICT_MAPPER = JacksonMapper.ofYaml()
|
||||
.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
|
||||
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
|
||||
@@ -33,12 +31,11 @@ public class YamlParser {
|
||||
return FilenameUtils.getExtension(path.toFile().getAbsolutePath()).equals("yaml") || FilenameUtils.getExtension(path.toFile().getAbsolutePath()).equals("yml");
|
||||
}
|
||||
|
||||
public <T> T parse(String input, Class<T> cls) {
|
||||
public static <T> T parse(String input, Class<T> cls) {
|
||||
return read(input, cls, type(cls));
|
||||
}
|
||||
|
||||
|
||||
public <T> T parse(Map<String, Object> input, Class<T> cls, Boolean strict) {
|
||||
public static <T> T parse(Map<String, Object> input, Class<T> cls, Boolean strict) {
|
||||
ObjectMapper currentMapper = strict ? STRICT_MAPPER : NON_STRICT_MAPPER;
|
||||
|
||||
try {
|
||||
@@ -56,7 +53,7 @@ public class YamlParser {
|
||||
return cls.getSimpleName().toLowerCase();
|
||||
}
|
||||
|
||||
public <T> T parse(File file, Class<T> cls) throws ConstraintViolationException {
|
||||
public static <T> T parse(File file, Class<T> cls) throws ConstraintViolationException {
|
||||
try {
|
||||
String input = IOUtils.toString(file.toURI(), StandardCharsets.UTF_8);
|
||||
return read(input, cls, type(cls));
|
||||
@@ -77,13 +74,12 @@ public class YamlParser {
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T read(String input, Class<T> objectClass, String resource) {
|
||||
private static <T> T read(String input, Class<T> objectClass, String resource) {
|
||||
try {
|
||||
return STRICT_MAPPER.readValue(input, objectClass);
|
||||
} catch (JsonProcessingException e) {
|
||||
jsonProcessingExceptionHandler(input, resource, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.conditions.ScheduleCondition;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.tasks.ResolvedTask;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.multipleflows.MultipleCondition;
|
||||
@@ -32,7 +33,7 @@ public class ConditionService {
|
||||
private RunContextFactory runContextFactory;
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean isValid(Condition condition, Flow flow, @Nullable Execution execution, MultipleConditionStorageInterface multipleConditionStorage) {
|
||||
public boolean isValid(Condition condition, FlowInterface flow, @Nullable Execution execution, MultipleConditionStorageInterface multipleConditionStorage) {
|
||||
ConditionContext conditionContext = this.conditionContext(
|
||||
runContextFactory.of(flow, execution),
|
||||
flow,
|
||||
@@ -43,11 +44,11 @@ public class ConditionService {
|
||||
return this.valid(flow, Collections.singletonList(condition), conditionContext);
|
||||
}
|
||||
|
||||
public boolean isValid(Condition condition, Flow flow, @Nullable Execution execution) {
|
||||
public boolean isValid(Condition condition, FlowInterface flow, @Nullable Execution execution) {
|
||||
return this.isValid(condition, flow, execution, null);
|
||||
}
|
||||
|
||||
private void logException(Flow flow, Object condition, ConditionContext conditionContext, Exception e) {
|
||||
private void logException(FlowInterface flow, Object condition, ConditionContext conditionContext, Exception e) {
|
||||
conditionContext.getRunContext().logger().warn(
|
||||
"[namespace: {}] [flow: {}] [condition: {}] Evaluate Condition Failed with error '{}'",
|
||||
flow.getNamespace(),
|
||||
@@ -116,7 +117,7 @@ public class ConditionService {
|
||||
}
|
||||
}
|
||||
|
||||
public ConditionContext conditionContext(RunContext runContext, Flow flow, @Nullable Execution execution, MultipleConditionStorageInterface multipleConditionStorage) {
|
||||
public ConditionContext conditionContext(RunContext runContext, FlowInterface flow, @Nullable Execution execution, MultipleConditionStorageInterface multipleConditionStorage) {
|
||||
return ConditionContext.builder()
|
||||
.flow(flow)
|
||||
.execution(execution)
|
||||
@@ -129,7 +130,7 @@ public class ConditionService {
|
||||
return this.conditionContext(runContext, flow, execution, null);
|
||||
}
|
||||
|
||||
boolean valid(Flow flow, List<Condition> list, ConditionContext conditionContext) {
|
||||
boolean valid(FlowInterface flow, List<Condition> list, ConditionContext conditionContext) {
|
||||
return list
|
||||
.stream()
|
||||
.allMatch(condition -> {
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.kestra.core.models.executions.ExecutionKilledExecution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.executions.TaskRunAttempt;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.flows.input.InputAndValue;
|
||||
@@ -319,7 +320,7 @@ public class ExecutionService {
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private Execution markAs(final Execution execution, Flow flow, String taskRunId, State.Type newState, @Nullable Map<String, Object> onResumeInputs) throws Exception {
|
||||
private Execution markAs(final Execution execution, FlowInterface flow, String taskRunId, State.Type newState, @Nullable Map<String, Object> onResumeInputs) throws Exception {
|
||||
Set<String> taskRunToRestart = this.taskRunToRestart(
|
||||
execution,
|
||||
taskRun -> taskRun.getId().equals(taskRunId)
|
||||
@@ -327,9 +328,11 @@ public class ExecutionService {
|
||||
|
||||
Execution newExecution = execution.withMetadata(execution.getMetadata().nextAttempt());
|
||||
|
||||
final FlowWithSource flowWithSource = pluginDefaultService.injectVersionDefaults(flow, false);
|
||||
|
||||
for (String s : taskRunToRestart) {
|
||||
TaskRun originalTaskRun = newExecution.findTaskRunByTaskRunId(s);
|
||||
Task task = flow.findTaskByTaskId(originalTaskRun.getTaskId());
|
||||
Task task = flowWithSource.findTaskByTaskId(originalTaskRun.getTaskId());
|
||||
boolean isFlowable = task.isFlowable();
|
||||
|
||||
if (!isFlowable || s.equals(taskRunId)) {
|
||||
@@ -477,7 +480,7 @@ public class ExecutionService {
|
||||
* @return the execution in the new state.
|
||||
* @throws Exception if the state of the execution cannot be updated
|
||||
*/
|
||||
public Execution resume(Execution execution, Flow flow, State.Type newState) throws Exception {
|
||||
public Execution resume(Execution execution, FlowInterface flow, State.Type newState) throws Exception {
|
||||
return this.resume(execution, flow, newState, (Map<String, Object>) null);
|
||||
}
|
||||
|
||||
@@ -490,7 +493,7 @@ public class ExecutionService {
|
||||
* @param flow the flow of the execution
|
||||
* @return the execution in the new state.
|
||||
*/
|
||||
public Mono<List<InputAndValue>> validateForResume(final Execution execution, Flow flow) {
|
||||
public Mono<List<InputAndValue>> validateForResume(final Execution execution, FlowInterface flow) {
|
||||
return getFirstPausedTaskOr(execution, flow)
|
||||
.flatMap(task -> {
|
||||
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
|
||||
@@ -532,7 +535,7 @@ public class ExecutionService {
|
||||
* @param inputs the onResume inputs
|
||||
* @return the execution in the new state.
|
||||
*/
|
||||
public Mono<Execution> resume(final Execution execution, Flow flow, State.Type newState, @Nullable Publisher<CompletedPart> inputs) {
|
||||
public Mono<Execution> resume(final Execution execution, FlowInterface flow, State.Type newState, @Nullable Publisher<CompletedPart> inputs) {
|
||||
return getFirstPausedTaskOr(execution, flow)
|
||||
.flatMap(task -> {
|
||||
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
|
||||
@@ -550,12 +553,14 @@ public class ExecutionService {
|
||||
});
|
||||
}
|
||||
|
||||
private static Mono<Optional<Task>> getFirstPausedTaskOr(Execution execution, Flow flow){
|
||||
private Mono<Optional<Task>> getFirstPausedTaskOr(Execution execution, FlowInterface flow){
|
||||
final FlowWithSource flowWithSource = pluginDefaultService.injectVersionDefaults(flow, false);
|
||||
|
||||
return Mono.create(sink -> {
|
||||
try {
|
||||
var runningTaskRun = execution
|
||||
.findFirstByState(State.Type.PAUSED)
|
||||
.map(throwFunction(task -> flow.findTaskByTaskId(task.getTaskId())));
|
||||
.map(throwFunction(task -> flowWithSource.findTaskByTaskId(task.getTaskId())));
|
||||
sink.success(runningTaskRun);
|
||||
} catch (InternalException e) {
|
||||
sink.error(e);
|
||||
@@ -574,7 +579,7 @@ public class ExecutionService {
|
||||
* @return the execution in the new state.
|
||||
* @throws Exception if the state of the execution cannot be updated
|
||||
*/
|
||||
public Execution resume(final Execution execution, Flow flow, State.Type newState, @Nullable Map<String, Object> inputs) throws Exception {
|
||||
public Execution resume(final Execution execution, FlowInterface flow, State.Type newState, @Nullable Map<String, Object> inputs) throws Exception {
|
||||
var pausedTaskRun = execution
|
||||
.findFirstByState(State.Type.PAUSED);
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.models.validations.ValidateConstraintViolation;
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
@@ -22,7 +25,17 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -30,27 +43,99 @@ import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Provides business logic to manipulate {@link Flow}
|
||||
* Provides business logic for manipulating flow objects.
|
||||
*/
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class FlowService {
|
||||
private static final ObjectMapper NON_DEFAULT_OBJECT_MAPPER = JacksonMapper.ofJson()
|
||||
.copy()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
|
||||
|
||||
@Inject
|
||||
Optional<FlowRepositoryInterface> flowRepository;
|
||||
|
||||
@Inject
|
||||
YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
PluginDefaultService pluginDefaultService;
|
||||
|
||||
@Inject
|
||||
PluginRegistry pluginRegistry;
|
||||
|
||||
@Inject
|
||||
ModelValidator modelValidator;
|
||||
|
||||
/**
|
||||
* Validates and creates the given flow.
|
||||
* <p>
|
||||
* The validation of the flow is done from the source after injecting all plugin default values.
|
||||
*
|
||||
* @param flow The flow.
|
||||
* @param strictValidation Specifies whether to perform a strict validation of the flow.
|
||||
* @return The created {@link FlowWithSource}.
|
||||
*/
|
||||
public FlowWithSource create(final GenericFlow flow, final boolean strictValidation) {
|
||||
Objects.requireNonNull(flow, "Cannot create null flow");
|
||||
if (flow.getSource() == null || flow.getSource().isBlank()) {
|
||||
throw new IllegalArgumentException("Cannot create flow with null or blank source");
|
||||
}
|
||||
|
||||
// Check Flow with defaults
|
||||
FlowWithSource flowWithDefault = pluginDefaultService.injectAllDefaults(flow, strictValidation);
|
||||
modelValidator.validate(flowWithDefault);
|
||||
|
||||
return repository().create(flow);
|
||||
}
|
||||
|
||||
private FlowRepositoryInterface repository() {
|
||||
return flowRepository
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot perform operation on flow. Cause: No FlowRepository"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given flow source.
|
||||
* <p>
|
||||
* the YAML source can contain one or many objects.
|
||||
*
|
||||
* @param tenantId The tenant identifier.
|
||||
* @param flows The YAML source.
|
||||
* @return The list validation constraint violations.
|
||||
*/
|
||||
public List<ValidateConstraintViolation> validate(final String tenantId, final String flows) {
|
||||
AtomicInteger index = new AtomicInteger(0);
|
||||
return Stream
|
||||
.of(flows.split("\\n+---\\n*?"))
|
||||
.map(source -> {
|
||||
ValidateConstraintViolation.ValidateConstraintViolationBuilder<?, ?> validateConstraintViolationBuilder = ValidateConstraintViolation.builder();
|
||||
validateConstraintViolationBuilder.index(index.getAndIncrement());
|
||||
|
||||
try {
|
||||
FlowWithSource flow = pluginDefaultService.parseFlowWithAllDefaults(tenantId, source, true);
|
||||
Integer sentRevision = flow.getRevision();
|
||||
if (sentRevision != null) {
|
||||
Integer lastRevision = Optional.ofNullable(repository().lastRevision(tenantId, flow.getNamespace(), flow.getId()))
|
||||
.orElse(0);
|
||||
validateConstraintViolationBuilder.outdated(!sentRevision.equals(lastRevision + 1));
|
||||
}
|
||||
|
||||
validateConstraintViolationBuilder.deprecationPaths(deprecationPaths(flow));
|
||||
validateConstraintViolationBuilder.warnings(warnings(flow, tenantId));
|
||||
validateConstraintViolationBuilder.infos(relocations(source).stream().map(relocation -> relocation.from() + " is replaced by " + relocation.to()).toList());
|
||||
validateConstraintViolationBuilder.flow(flow.getId());
|
||||
validateConstraintViolationBuilder.namespace(flow.getNamespace());
|
||||
|
||||
modelValidator.validate(flow);
|
||||
} catch (ConstraintViolationException e) {
|
||||
validateConstraintViolationBuilder.constraints(e.getMessage());
|
||||
} catch (RuntimeException re) {
|
||||
// In case of any error, we add a validation violation so the error is displayed in the UI.
|
||||
// We may change that by throwing an internal error and handle it in the UI, but this should not occur except for rare cases
|
||||
// in dev like incompatible plugin versions.
|
||||
log.error("Unable to validate the flow", re);
|
||||
validateConstraintViolationBuilder.constraints("Unable to validate the flow: " + re.getMessage());
|
||||
}
|
||||
|
||||
return validateConstraintViolationBuilder.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public FlowWithSource importFlow(String tenantId, String source) {
|
||||
return this.importFlow(tenantId, source, false);
|
||||
}
|
||||
@@ -60,29 +145,33 @@ public class FlowService {
|
||||
throw noRepositoryException();
|
||||
}
|
||||
|
||||
FlowWithSource withTenant = yamlParser.parse(source, Flow.class).toBuilder()
|
||||
.tenantId(tenantId)
|
||||
.build()
|
||||
.withSource(source);
|
||||
final GenericFlow flow = GenericFlow.fromYaml(tenantId, source);
|
||||
|
||||
FlowRepositoryInterface flowRepository = this.flowRepository.get();
|
||||
Optional<FlowWithSource> flowWithSource = flowRepository
|
||||
.findByIdWithSource(withTenant.getTenantId(), withTenant.getNamespace(), withTenant.getId(), Optional.empty(), true);
|
||||
if (dryRun) {
|
||||
return flowWithSource
|
||||
.map(previous -> {
|
||||
if (previous.equals(withTenant, source) && !previous.isDeleted()) {
|
||||
return previous;
|
||||
} else {
|
||||
return FlowWithSource.of(withTenant.toBuilder().revision(previous.getRevision() + 1).build(), source);
|
||||
}
|
||||
})
|
||||
.orElseGet(() -> FlowWithSource.of(withTenant, source).toBuilder().revision(1).build());
|
||||
}
|
||||
Optional<FlowWithSource> maybeExisting = flowRepository.findByIdWithSource(
|
||||
flow.getTenantId(),
|
||||
flow.getNamespace(),
|
||||
flow.getId(),
|
||||
Optional.empty(),
|
||||
true
|
||||
);
|
||||
|
||||
return flowWithSource
|
||||
.map(previous -> flowRepository.update(withTenant, previous, source, pluginDefaultService.injectDefaults(withTenant)))
|
||||
.orElseGet(() -> flowRepository.create(withTenant, source, pluginDefaultService.injectDefaults(withTenant)));
|
||||
// Inject default plugin 'version' props before converting
|
||||
// to flow to correctly resolve all plugin type.
|
||||
FlowWithSource flowToImport = pluginDefaultService.injectVersionDefaults(flow, false);
|
||||
|
||||
if (dryRun) {
|
||||
return maybeExisting
|
||||
.map(previous -> previous.isSameWithSource(flowToImport) && !previous.isDeleted() ?
|
||||
previous :
|
||||
FlowWithSource.of(flowToImport.toBuilder().revision(previous.getRevision() + 1).build(), source)
|
||||
)
|
||||
.orElseGet(() -> FlowWithSource.of(flowToImport, source).toBuilder().revision(1).build());
|
||||
} else {
|
||||
return maybeExisting
|
||||
.map(previous -> flowRepository.update(flow, previous))
|
||||
.orElseGet(() -> flowRepository.create(flow));
|
||||
}
|
||||
}
|
||||
|
||||
public List<FlowWithSource> findByNamespaceWithSource(String tenantId, String namespace) {
|
||||
@@ -117,7 +206,7 @@ public class FlowService {
|
||||
return flowRepository.get().findById(tenantId, namespace, flowId);
|
||||
}
|
||||
|
||||
public Stream<FlowWithSource> keepLastVersion(Stream<FlowWithSource> stream) {
|
||||
public Stream<FlowInterface> keepLastVersion(Stream<FlowInterface> stream) {
|
||||
return keepLastVersionCollector(stream);
|
||||
}
|
||||
|
||||
@@ -132,6 +221,15 @@ public class FlowService {
|
||||
}
|
||||
|
||||
List<String> warnings = new ArrayList<>(checkValidSubflows(flow, tenantId));
|
||||
List<io.kestra.plugin.core.trigger.Flow> flowTriggers = ListUtils.emptyOnNull(flow.getTriggers()).stream()
|
||||
.filter(io.kestra.plugin.core.trigger.Flow.class::isInstance)
|
||||
.map(io.kestra.plugin.core.trigger.Flow.class::cast)
|
||||
.toList();
|
||||
flowTriggers.forEach(flowTrigger -> {
|
||||
if (ListUtils.emptyOnNull(flowTrigger.getConditions()).isEmpty() && flowTrigger.getPreconditions() == null) {
|
||||
warnings.add("This flow will be triggered for EVERY execution of EVERY flow on your instance. We recommend adding the preconditions property to the Flow trigger '" + flowTrigger.getId() + "'.");
|
||||
}
|
||||
});
|
||||
|
||||
return warnings;
|
||||
}
|
||||
@@ -140,7 +238,11 @@ public class FlowService {
|
||||
try {
|
||||
Map<String, Class<?>> aliases = pluginRegistry.plugins().stream()
|
||||
.flatMap(plugin -> plugin.getAliases().values().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
Map.Entry::getValue,
|
||||
(existing, duplicate) -> existing
|
||||
));
|
||||
Map<String, Object> stringObjectMap = JacksonMapper.ofYaml().readValue(flowSource, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||
return relocations(aliases, stringObjectMap);
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -249,17 +351,17 @@ public class FlowService {
|
||||
.filter(method -> !Modifier.isStatic(method.getModifiers()));
|
||||
}
|
||||
|
||||
public Collection<FlowWithSource> keepLastVersion(List<FlowWithSource> flows) {
|
||||
public Collection<FlowInterface> keepLastVersion(List<FlowInterface> flows) {
|
||||
return keepLastVersionCollector(flows.stream()).toList();
|
||||
}
|
||||
|
||||
public Stream<FlowWithSource> keepLastVersionCollector(Stream<FlowWithSource> stream) {
|
||||
public Stream<FlowInterface> keepLastVersionCollector(Stream<FlowInterface> stream) {
|
||||
// Use a Map to track the latest version of each flow
|
||||
Map<String, FlowWithSource> latestFlows = new HashMap<>();
|
||||
Map<String, FlowInterface> latestFlows = new HashMap<>();
|
||||
|
||||
stream.forEach(flow -> {
|
||||
String uid = flow.uidWithoutRevision();
|
||||
FlowWithSource existing = latestFlows.get(uid);
|
||||
FlowInterface existing = latestFlows.get(uid);
|
||||
|
||||
// Update only if the current flow has a higher revision
|
||||
if (existing == null || flow.getRevision() > existing.getRevision()) {
|
||||
@@ -276,7 +378,7 @@ public class FlowService {
|
||||
|
||||
protected boolean removeUnwanted(Flow f, Execution execution) {
|
||||
// we don't allow recursive
|
||||
return !f.uidWithoutRevision().equals(Flow.uidWithoutRevision(execution));
|
||||
return !f.uidWithoutRevision().equals(FlowId.uidWithoutRevision(execution));
|
||||
}
|
||||
|
||||
public static List<AbstractTrigger> findRemovedTrigger(Flow flow, Flow previous) {
|
||||
@@ -314,22 +416,6 @@ public class FlowService {
|
||||
return source + String.format("\ndisabled: %s", disabled);
|
||||
}
|
||||
|
||||
public static String generateSource(Flow flow) {
|
||||
try {
|
||||
String json = NON_DEFAULT_OBJECT_MAPPER.writeValueAsString(flow);
|
||||
|
||||
Object map = fixSnakeYaml(JacksonMapper.toMap(json));
|
||||
|
||||
String source = JacksonMapper.ofYaml().writeValueAsString(map);
|
||||
|
||||
// remove the revision from the generated source
|
||||
return source.replaceFirst("(?m)^revision: \\d+\n?","");
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Unable to convert flow json '{}' '{}'({})", flow.getNamespace(), flow.getId(), flow.getRevision(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Git plugin
|
||||
public List<Flow> findByNamespacePrefix(String tenantId, String namespacePrefix) {
|
||||
if (flowRepository.isEmpty()) {
|
||||
@@ -348,50 +434,6 @@ public class FlowService {
|
||||
return flowRepository.get().delete(flow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dirty hack but only concern previous flow with no source code in org.yaml.snakeyaml.emitter.Emitter:
|
||||
* <pre>
|
||||
* if (previousSpace) {
|
||||
* spaceBreak = true;
|
||||
* }
|
||||
* </pre>
|
||||
* This control will detect ` \n` as a no valid entry on a string and will break the multiline to transform in single line
|
||||
*
|
||||
* @param object the object to fix
|
||||
* @return the modified object
|
||||
*/
|
||||
private static Object fixSnakeYaml(Object object) {
|
||||
if (object instanceof Map<?, ?> mapValue) {
|
||||
return mapValue
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> new AbstractMap.SimpleEntry<>(
|
||||
fixSnakeYaml(entry.getKey()),
|
||||
fixSnakeYaml(entry.getValue())
|
||||
))
|
||||
.filter(entry -> entry.getValue() != null)
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
Map.Entry::getValue,
|
||||
(u, v) -> {
|
||||
throw new IllegalStateException(String.format("Duplicate key %s", u));
|
||||
},
|
||||
LinkedHashMap::new
|
||||
));
|
||||
} else if (object instanceof Collection<?> collectionValue) {
|
||||
return collectionValue
|
||||
.stream()
|
||||
.map(FlowService::fixSnakeYaml)
|
||||
.toList();
|
||||
} else if (object instanceof String item) {
|
||||
if (item.contains("\n")) {
|
||||
return item.replaceAll("\\s+\\n", "\\\n");
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the namespace is allowed from the namespace denoted by 'fromTenant' and 'fromNamespace'.
|
||||
* As namespace restriction is an EE feature, this will always return true in OSS.
|
||||
|
||||
@@ -49,7 +49,7 @@ public class FlowTriggerService {
|
||||
.map(io.kestra.plugin.core.trigger.Flow.class::cast);
|
||||
}
|
||||
|
||||
public List<Execution> computeExecutionsFromFlowTriggers(Execution execution, List<Flow> allFlows, Optional<MultipleConditionStorageInterface> multipleConditionStorage) {
|
||||
public List<Execution> computeExecutionsFromFlowTriggers(Execution execution, List<? extends Flow> allFlows, Optional<MultipleConditionStorageInterface> multipleConditionStorage) {
|
||||
List<FlowWithFlowTrigger> validTriggersBeforeMultipleConditionEval = allFlows.stream()
|
||||
// prevent recursive flow triggers
|
||||
.filter(flow -> flowService.removeUnwanted(flow, execution))
|
||||
|
||||
@@ -56,7 +56,7 @@ public class GraphService {
|
||||
|
||||
public GraphCluster of(GraphCluster baseGraph, FlowWithSource flow, List<String> expandedSubflows, Map<String, FlowWithSource> flowByUid, Execution execution) throws IllegalVariableEvaluationException {
|
||||
String tenantId = flow.getTenantId();
|
||||
flow = pluginDefaultService.injectDefaults(flow);
|
||||
flow = pluginDefaultService.injectAllDefaults(flow, false);
|
||||
List<Trigger> triggers = null;
|
||||
if (flow.getTriggers() != null) {
|
||||
triggers = triggerRepository.find(Pageable.UNPAGED, null, tenantId, flow.getNamespace(), flow.getId(), null);
|
||||
@@ -120,7 +120,7 @@ public class GraphService {
|
||||
));
|
||||
}
|
||||
);
|
||||
subflow = pluginDefaultService.injectDefaults(subflow);
|
||||
subflow = pluginDefaultService.injectAllDefaults(subflow, false);
|
||||
|
||||
SubflowGraphTask finalSubflowGraphTask = subflowGraphTask;
|
||||
return new TaskToClusterReplacer(
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
@@ -17,7 +17,7 @@ public final class LabelService {
|
||||
/**
|
||||
* Return flow labels excluding system labels.
|
||||
*/
|
||||
public static List<Label> labelsExcludingSystem(Flow flow) {
|
||||
public static List<Label> labelsExcludingSystem(FlowInterface flow) {
|
||||
return ListUtils.emptyOnNull(flow.getLabels()).stream().filter(label -> !label.key().startsWith(Label.SYSTEM_PREFIX)).toList();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public final class LabelService {
|
||||
* Trigger labels will be rendered via the run context but not flow labels.
|
||||
* In case rendering is not possible, the label will be omitted.
|
||||
*/
|
||||
public static List<Label> fromTrigger(RunContext runContext, Flow flow, AbstractTrigger trigger) {
|
||||
public static List<Label> fromTrigger(RunContext runContext, FlowInterface flow, AbstractTrigger trigger) {
|
||||
final List<Label> labels = new ArrayList<>();
|
||||
|
||||
if (flow.getLabels() != null) {
|
||||
|
||||
@@ -3,7 +3,8 @@ package io.kestra.core.services;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.triggers.TriggerContext;
|
||||
import io.kestra.core.repositories.LogRepositoryInterface;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
@@ -39,7 +40,7 @@ public class LogService {
|
||||
@Inject
|
||||
private LogRepositoryInterface logRepository;
|
||||
|
||||
public void logExecution(Flow flow, Logger logger, Level level, String message, Object... args) {
|
||||
public void logExecution(FlowId flow, Logger logger, Level level, String message, Object... args) {
|
||||
String finalMsg = tenantEnabled ? FLOW_PREFIX_WITH_TENANT + message : FLOW_PREFIX_NO_TENANT + message;
|
||||
Object[] executionArgs = tenantEnabled ?
|
||||
new Object[] { flow.getTenantId(), flow.getNamespace(), flow.getId() } :
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.utils.NamespaceUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -19,7 +21,7 @@ public class NamespaceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given namespace exists.
|
||||
* Checks whether a given namespace exists. A namespace is considered existing if at least one Flow is within the namespace or a parent namespace
|
||||
*
|
||||
* @param tenant The tenant ID
|
||||
* @param namespace The namespace - cannot be null.
|
||||
@@ -29,7 +31,10 @@ public class NamespaceService {
|
||||
Objects.requireNonNull(namespace, "namespace cannot be null");
|
||||
|
||||
if (flowRepository.isPresent()) {
|
||||
List<String> namespaces = flowRepository.get().findDistinctNamespace(tenant);
|
||||
List<String> namespaces = flowRepository.get().findDistinctNamespace(tenant).stream()
|
||||
.map(NamespaceUtils::asTree)
|
||||
.flatMap(Collection::stream)
|
||||
.toList();
|
||||
return namespaces.stream().anyMatch(ns -> ns.equals(namespace) || ns.startsWith(namespace));
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -2,13 +2,17 @@ package io.kestra.core.services;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Lists;
|
||||
import io.kestra.core.exceptions.KestraRuntimeException;
|
||||
import io.kestra.core.models.Plugin;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.PluginDefault;
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
@@ -19,22 +23,34 @@ import io.kestra.core.runners.RunContextLogger;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import io.kestra.plugin.core.flow.Template;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Provider;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
/**
|
||||
* Services for parsing flows and injecting plugin default values.
|
||||
*/
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class PluginDefaultService {
|
||||
@@ -44,6 +60,10 @@ public class PluginDefaultService {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = JacksonMapper.ofYaml().copy()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
private static final String PLUGIN_DEFAULTS_FIELD = "pluginDefaults";
|
||||
|
||||
private static final TypeReference<List<PluginDefault>> PLUGIN_DEFAULTS_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
@Nullable
|
||||
@Inject
|
||||
@@ -53,16 +73,16 @@ public class PluginDefaultService {
|
||||
@Inject
|
||||
protected PluginGlobalDefaultConfiguration pluginGlobalDefault;
|
||||
|
||||
@Inject
|
||||
protected YamlParser yamlParser;
|
||||
|
||||
@Inject
|
||||
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
|
||||
@Nullable
|
||||
protected QueueInterface<LogEntry> logQueue;
|
||||
|
||||
@Inject
|
||||
private PluginRegistry pluginRegistry;
|
||||
protected PluginRegistry pluginRegistry;
|
||||
|
||||
@Inject
|
||||
protected Provider<LogService> logService; // lazy-init
|
||||
|
||||
private final AtomicBoolean warnOnce = new AtomicBoolean(false);
|
||||
|
||||
@@ -83,38 +103,69 @@ public class PluginDefaultService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the defaults values for the given flow.
|
||||
*
|
||||
* @param flow the flow to extract default
|
||||
* @return list of {@code PluginDefault} ordered by most important first
|
||||
*/
|
||||
protected List<PluginDefault> mergeAllDefaults(Flow flow) {
|
||||
List<PluginDefault> list = new ArrayList<>();
|
||||
protected List<PluginDefault> getAllDefaults(final String tenantId,
|
||||
final String namespace,
|
||||
final Map<String, Object> flow) {
|
||||
List<PluginDefault> defaults = new ArrayList<>();
|
||||
defaults.addAll(getFlowDefaults(flow));
|
||||
defaults.addAll(getGlobalDefaults());
|
||||
return defaults;
|
||||
}
|
||||
|
||||
if (flow.getPluginDefaults() != null) {
|
||||
list.addAll(flow.getPluginDefaults());
|
||||
/**
|
||||
* Gets the flow-level defaults values.
|
||||
*
|
||||
* @param flow the flow to extract default
|
||||
* @return list of {@code PluginDefault} ordered by most important first
|
||||
*/
|
||||
protected List<PluginDefault> getFlowDefaults(final Map<String, Object> flow) {
|
||||
Object defaults = flow.get(PLUGIN_DEFAULTS_FIELD);
|
||||
if (defaults != null) {
|
||||
return OBJECT_MAPPER.convertValue(defaults, PLUGIN_DEFAULTS_TYPE_REF);
|
||||
} else {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global defaults values.
|
||||
*
|
||||
* @return list of {@code PluginDefault} ordered by most important first
|
||||
*/
|
||||
protected List<PluginDefault> getGlobalDefaults() {
|
||||
List<PluginDefault> defaults = new ArrayList<>();
|
||||
|
||||
if (taskGlobalDefault != null && taskGlobalDefault.getDefaults() != null) {
|
||||
if (warnOnce.compareAndSet(false, true)) {
|
||||
log.warn("Global Task Defaults are deprecated, please use Global Plugin Defaults instead via the 'kestra.plugins.defaults' configuration property.");
|
||||
}
|
||||
list.addAll(taskGlobalDefault.getDefaults());
|
||||
defaults.addAll(taskGlobalDefault.getDefaults());
|
||||
}
|
||||
|
||||
if (pluginGlobalDefault != null && pluginGlobalDefault.getDefaults() != null) {
|
||||
list.addAll(pluginGlobalDefault.getDefaults());
|
||||
defaults.addAll(pluginGlobalDefault.getDefaults());
|
||||
}
|
||||
|
||||
return list;
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject plugin defaults into a Flow.
|
||||
* In case of exception, the flow is returned as is,
|
||||
* then a logger is created based on the execution to be able to log an exception in the execution logs.
|
||||
* Parses the given abstract flow and injects all default values, returning a parsed {@link FlowWithSource}.
|
||||
*
|
||||
* <p>
|
||||
* If an exception occurs during parsing, the original flow is returned unchanged, and the exception is logged
|
||||
* for the passed {@code execution}
|
||||
* </p>
|
||||
*
|
||||
* @return a parsed {@link FlowWithSource}, or a {@link FlowWithException} if parsing fails
|
||||
*/
|
||||
public FlowWithSource injectDefaults(FlowWithSource flow, Execution execution) {
|
||||
public FlowWithSource injectDefaults(FlowInterface flow, Execution execution) {
|
||||
try {
|
||||
return this.injectDefaults(flow);
|
||||
return this.injectAllDefaults(flow, false);
|
||||
} catch (Exception e) {
|
||||
RunContextLogger
|
||||
.logEntries(
|
||||
@@ -128,86 +179,232 @@ public class PluginDefaultService {
|
||||
// silently do nothing
|
||||
}
|
||||
});
|
||||
return flow;
|
||||
return readWithoutDefaultsOrThrow(flow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #injectDefaults(FlowWithSource, Logger)} instead
|
||||
* Parses the given abstract flow and injects all default values, returning a parsed {@link FlowWithSource}.
|
||||
*
|
||||
* <p>
|
||||
* If an exception occurs during parsing, the original flow is returned unchanged, and the exception is logged.
|
||||
* </p>
|
||||
*
|
||||
* @return a parsed {@link FlowWithSource}, or a {@link FlowWithException} if parsing fails
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "0.20")
|
||||
public Flow injectDefaults(Flow flow, Logger logger) {
|
||||
public FlowWithSource injectAllDefaults(FlowInterface flow, Logger logger) {
|
||||
try {
|
||||
return this.injectDefaults(flow);
|
||||
return this.injectAllDefaults(flow, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn(e.getMessage(), e);
|
||||
return flow;
|
||||
logger.warn(
|
||||
"Can't inject plugin defaults on tenant {}, namespace '{}', flow '{}' with errors '{}'",
|
||||
flow.getTenantId(),
|
||||
flow.getNamespace(),
|
||||
flow.getId(),
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
return readWithoutDefaultsOrThrow(flow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject plugin defaults into a Flow.
|
||||
* In case of exception, the flow is returned as is, then the logger is used to log the exception.
|
||||
*/
|
||||
public FlowWithSource injectDefaults(FlowWithSource flow, Logger logger) {
|
||||
private static FlowWithSource readWithoutDefaultsOrThrow(final FlowInterface flow) {
|
||||
if (flow instanceof FlowWithSource item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (flow instanceof Flow item) {
|
||||
return FlowWithSource.of(item, item.sourceOrGenerateIfNull());
|
||||
}
|
||||
|
||||
// The block below should only be reached during testing for failure scenarios
|
||||
try {
|
||||
return this.injectDefaults(flow);
|
||||
} catch (Exception e) {
|
||||
logger.warn(e.getMessage(), e);
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #injectDefaults(FlowWithSource)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "0.20")
|
||||
public Flow injectDefaults(Flow flow) throws ConstraintViolationException {
|
||||
if (flow instanceof FlowWithSource flowWithSource) {
|
||||
return this.injectDefaults(flowWithSource);
|
||||
}
|
||||
|
||||
Map<String, Object> flowAsMap = NON_DEFAULT_OBJECT_MAPPER.convertValue(flow, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||
|
||||
return innerInjectDefault(flow, flowAsMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject plugin defaults into a Flow.
|
||||
*/
|
||||
public FlowWithSource injectDefaults(FlowWithSource flow) throws ConstraintViolationException {
|
||||
try {
|
||||
String source = flow.getSource();
|
||||
if (source == null) {
|
||||
// Flow revisions created from older Kestra versions may not be linked to their original source.
|
||||
// In such cases, fall back to the generated source approach to enable plugin default injection.
|
||||
source = flow.generateSource();
|
||||
}
|
||||
|
||||
if (source == null) {
|
||||
// return immediately if source is still null (should never happen)
|
||||
return flow;
|
||||
}
|
||||
|
||||
Map<String, Object> flowAsMap = OBJECT_MAPPER.readValue(source, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||
|
||||
Flow withDefault = innerInjectDefault(flow, flowAsMap);
|
||||
|
||||
// revision and tenants are not in the source, so we copy them manually
|
||||
return withDefault.toBuilder()
|
||||
.tenantId(flow.getTenantId())
|
||||
.revision(flow.getRevision())
|
||||
.build()
|
||||
.withSource(source);
|
||||
Flow parsed = NON_DEFAULT_OBJECT_MAPPER.readValue(flow.getSource(), Flow.class);
|
||||
return FlowWithSource.of(parsed, flow.getSource());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new KestraRuntimeException("Failed to read flow from source", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given abstract flow and injects all default values, returning a parsed {@link FlowWithSource}.
|
||||
*
|
||||
* <p>
|
||||
* If {@code strictParsing} is {@code true}, the parsing will fail in the following cases:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>The source contains duplicate properties.</li>
|
||||
* <li>The source contains unknown properties.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param flow the flow to be parsed
|
||||
* @return a parsed {@link FlowWithSource}
|
||||
*
|
||||
* @throws ConstraintViolationException if {@code strictParsing} is {@code true} and the source does not meet strict validation requirements
|
||||
* @throws KestraRuntimeException if an error occurs while parsing the flow and it cannot be processed
|
||||
*/
|
||||
public FlowWithSource injectAllDefaults(final FlowInterface flow, final boolean strictParsing) {
|
||||
|
||||
// Flow revisions created from older Kestra versions may not be linked to their original source.
|
||||
// In such cases, fall back to the generated source approach to enable plugin default injection.
|
||||
String source = flow.sourceOrGenerateIfNull();
|
||||
|
||||
if (source == null) {
|
||||
// This should never happen
|
||||
String error = "Cannot apply plugin defaults. Cause: flow has no defined source.";
|
||||
logService.get().logExecution(flow, log, Level.ERROR, error);
|
||||
throw new IllegalArgumentException(error);
|
||||
}
|
||||
|
||||
return parseFlowWithAllDefaults(
|
||||
flow.getTenantId(),
|
||||
flow.getNamespace(),
|
||||
flow.getRevision(),
|
||||
flow.isDeleted(),
|
||||
source,
|
||||
false,
|
||||
strictParsing
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given abstract flow and injects default plugin versions, returning a parsed {@link FlowWithSource}.
|
||||
*
|
||||
* <p>
|
||||
* If the provided flow already represents a concrete {@link FlowWithSource}, it is returned as is.
|
||||
* <p/>
|
||||
*
|
||||
* <p>
|
||||
* If {@code safe} is set to {@code true} and the given flow cannot be parsed,
|
||||
* this method returns a {@link FlowWithException} instead of throwing an error.
|
||||
* <p/>
|
||||
*
|
||||
* @param flow the flow to be parsed
|
||||
* @param safe whether parsing errors should be handled gracefully
|
||||
* @return a parsed {@link FlowWithSource}, or a {@link FlowWithException} if parsing fails and {@code safe} is {@code true}
|
||||
*/
|
||||
public FlowWithSource injectVersionDefaults(final FlowInterface flow, final boolean safe) {
|
||||
if (flow instanceof FlowWithSource flowWithSource) {
|
||||
// shortcut - if the flow is already fully parsed return it immediately.
|
||||
return flowWithSource;
|
||||
}
|
||||
|
||||
FlowWithSource result;
|
||||
String source = flow.getSource();
|
||||
try {
|
||||
|
||||
if (source == null) {
|
||||
source = OBJECT_MAPPER.writeValueAsString(flow);
|
||||
}
|
||||
|
||||
result = parseFlowWithAllDefaults(flow.getTenantId(), flow.getNamespace(), flow.getRevision(), flow.isDeleted(), source, true, false);
|
||||
} catch (Exception e) {
|
||||
if (safe) {
|
||||
logService.get().logExecution(flow, log, Level.ERROR, "Failed to read flow.", e);
|
||||
result = FlowWithException.from(flow, e);
|
||||
|
||||
// deleted is not part of the original 'source'
|
||||
result = result.toBuilder().deleted(flow.isDeleted()).build();
|
||||
} else {
|
||||
throw new KestraRuntimeException(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> injectVersionDefaults(@Nullable final String tenantId,
|
||||
final String namespace,
|
||||
final Map<String, Object> mapFlow) {
|
||||
return innerInjectDefault(tenantId, namespace, mapFlow, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and injects default into the given flow.
|
||||
*
|
||||
* @param tenantId the Tenant ID.
|
||||
* @param source the flow source.
|
||||
* @return a new {@link FlowWithSource}.
|
||||
*
|
||||
* @throws ConstraintViolationException when parsing flow.
|
||||
*/
|
||||
public FlowWithSource parseFlowWithAllDefaults(@Nullable final String tenantId, final String source, final boolean strict) throws ConstraintViolationException {
|
||||
return parseFlowWithAllDefaults(tenantId, null, null, false, source, false, strict);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and injects defaults into the given flow.
|
||||
*
|
||||
* @param tenant the tenant identifier.
|
||||
* @param namespace the namespace.
|
||||
* @param revision the flow revision.
|
||||
* @param source the flow source.
|
||||
* @return a new {@link FlowWithSource}.
|
||||
*
|
||||
* @throws ConstraintViolationException when parsing flow.
|
||||
*/
|
||||
private FlowWithSource parseFlowWithAllDefaults(@Nullable final String tenant,
|
||||
@Nullable String namespace,
|
||||
@Nullable Integer revision,
|
||||
final boolean isDeleted,
|
||||
final String source,
|
||||
final boolean onlyVersions,
|
||||
final boolean strictParsing) throws ConstraintViolationException {
|
||||
try {
|
||||
Map<String, Object> mapFlow = OBJECT_MAPPER.readValue(source, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||
namespace = namespace == null ? (String) mapFlow.get("namespace") : namespace;
|
||||
revision = revision == null ? (Integer) mapFlow.get("revision") : revision;
|
||||
|
||||
mapFlow = innerInjectDefault(tenant, namespace, mapFlow, onlyVersions);
|
||||
|
||||
FlowWithSource withDefault = YamlParser.parse(mapFlow, FlowWithSource.class, strictParsing);
|
||||
|
||||
// revision, tenants, and deleted are not in the 'source', so we copy them manually
|
||||
FlowWithSource full = withDefault.toBuilder()
|
||||
.tenantId(tenant)
|
||||
.revision(revision)
|
||||
.deleted(isDeleted)
|
||||
.source(source)
|
||||
.build();
|
||||
|
||||
if (tenant != null) {
|
||||
// This is a hack to set the tenant in template tasks.
|
||||
// When using the Template task, we need the tenant to fetch the Template from the database.
|
||||
// However, as the task is executed on the Executor we cannot retrieve it from the tenant service and have no other options.
|
||||
// So we save it at flow creation/updating time.
|
||||
full.allTasksWithChilds().stream().filter(task -> task instanceof Template).forEach(task -> ((Template) task).setTenantId(tenant));
|
||||
}
|
||||
|
||||
return full;
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new KestraRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Flow innerInjectDefault(Flow flow, Map<String, Object> flowAsMap) {
|
||||
List<PluginDefault> allDefaults = mergeAllDefaults(flow);
|
||||
private Map<String, Object> innerInjectDefault(final String tenantId, final String namespace, Map<String, Object> flowAsMap, final boolean onlyVersions) {
|
||||
List<PluginDefault> allDefaults = getAllDefaults(tenantId, namespace, flowAsMap);
|
||||
|
||||
if (onlyVersions) {
|
||||
// filter only default 'version' property
|
||||
allDefaults = allDefaults.stream()
|
||||
.map(defaults -> {
|
||||
Map<String, Object> filtered = defaults.getValues().entrySet()
|
||||
.stream().filter(entry -> entry.getKey().equals("version"))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
return filtered.isEmpty() ? null : defaults.toBuilder().values(filtered).build();
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
if (allDefaults.isEmpty()) {
|
||||
// no defaults to inject - return immediately.
|
||||
return flowAsMap;
|
||||
}
|
||||
|
||||
addAliases(allDefaults);
|
||||
|
||||
Map<Boolean, List<PluginDefault>> allDefaultsGroup = allDefaults
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(PluginDefault::isForced, Collectors.toList()));
|
||||
@@ -218,9 +415,9 @@ public class PluginDefaultService {
|
||||
// forced plugin default need to be reverse, lower win
|
||||
Map<String, List<PluginDefault>> forced = pluginDefaultsToMap(Lists.reverse(allDefaultsGroup.getOrDefault(true, Collections.emptyList())));
|
||||
|
||||
Object pluginDefaults = flowAsMap.get("pluginDefaults");
|
||||
Object pluginDefaults = flowAsMap.get(PLUGIN_DEFAULTS_FIELD);
|
||||
if (pluginDefaults != null) {
|
||||
flowAsMap.remove("pluginDefaults");
|
||||
flowAsMap.remove(PLUGIN_DEFAULTS_FIELD);
|
||||
}
|
||||
|
||||
// we apply default and overwrite with forced
|
||||
@@ -233,10 +430,11 @@ public class PluginDefaultService {
|
||||
}
|
||||
|
||||
if (pluginDefaults != null) {
|
||||
flowAsMap.put("pluginDefaults", pluginDefaults);
|
||||
flowAsMap.put(PLUGIN_DEFAULTS_FIELD, pluginDefaults);
|
||||
}
|
||||
|
||||
return yamlParser.parse(flowAsMap, Flow.class, false);
|
||||
return flowAsMap;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +444,7 @@ public class PluginDefaultService {
|
||||
* validation will be disabled as we cannot differentiate between a prefix or an unknown type.
|
||||
*/
|
||||
public List<String> validateDefault(PluginDefault pluginDefault) {
|
||||
Class<? extends Plugin> classByIdentifier = pluginRegistry.findClassByIdentifier(pluginDefault.getType());
|
||||
Class<? extends Plugin> classByIdentifier = getClassByIdentifier(pluginDefault);
|
||||
if (classByIdentifier == null) {
|
||||
// this can either be a prefix or a non-existing plugin, in both cases we cannot validate in detail
|
||||
return Collections.emptyList();
|
||||
@@ -269,6 +467,10 @@ public class PluginDefaultService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
protected Class<? extends Plugin> getClassByIdentifier(PluginDefault pluginDefault) {
|
||||
return pluginRegistry.findClassByIdentifier(pluginDefault.getType());
|
||||
}
|
||||
|
||||
private Map<String, List<PluginDefault>> pluginDefaultsToMap(List<PluginDefault> pluginDefaults) {
|
||||
return pluginDefaults
|
||||
.stream()
|
||||
@@ -278,7 +480,7 @@ public class PluginDefaultService {
|
||||
private void addAliases(List<PluginDefault> allDefaults) {
|
||||
List<PluginDefault> aliasedPluginDefault = allDefaults.stream()
|
||||
.map(pluginDefault -> {
|
||||
Class<? extends Plugin> classByIdentifier = pluginRegistry.findClassByIdentifier(pluginDefault.getType());
|
||||
Class<? extends Plugin> classByIdentifier = getClassByIdentifier(pluginDefault);
|
||||
return classByIdentifier != null && !pluginDefault.getType().equals(classByIdentifier.getTypeName()) ? pluginDefault.toBuilder().type(classByIdentifier.getTypeName()).build() : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
@@ -343,4 +545,42 @@ public class PluginDefaultService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
// DEPRECATED
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #injectAllDefaults(FlowInterface, Logger)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "0.20")
|
||||
public Flow injectDefaults(Flow flow, Logger logger) {
|
||||
try {
|
||||
return this.injectDefaults(flow);
|
||||
} catch (Exception e) {
|
||||
logger.warn(
|
||||
"Can't inject plugin defaults on tenant {}, namespace '{}', flow '{}' with errors '{}'",
|
||||
flow.getTenantId(),
|
||||
flow.getNamespace(),
|
||||
flow.getId(),
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #injectAllDefaults(FlowInterface, boolean)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "0.20")
|
||||
public Flow injectDefaults(Flow flow) throws ConstraintViolationException {
|
||||
if (flow instanceof FlowWithSource flowWithSource) {
|
||||
return this.injectAllDefaults(flowWithSource, false);
|
||||
}
|
||||
|
||||
Map<String, Object> mapFlow = NON_DEFAULT_OBJECT_MAPPER.convertValue(flow, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||
mapFlow = innerInjectDefault(flow.getTenantId(), flow.getNamespace(), mapFlow, false);
|
||||
return YamlParser.parse(mapFlow, Flow.class, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import io.kestra.core.annotations.Retryable;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.Plugin;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package io.kestra.core.topologies;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.conditions.Condition;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.hierarchies.Graph;
|
||||
import io.kestra.core.models.tasks.ExecutableTask;
|
||||
@@ -29,7 +32,7 @@ import java.util.stream.Stream;
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class FlowTopologyService {
|
||||
public static final Label SIMULATED_EXECUTION = new Label(Label.SYSTEM_PREFIX + "simulatedExecution", "true");
|
||||
public static final Label SIMULATED_EXECUTION = new Label(Label.SIMULATED_EXECUTION, "true");
|
||||
|
||||
@Inject
|
||||
protected ConditionService conditionService;
|
||||
@@ -140,7 +143,8 @@ public class FlowTopologyService {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public FlowRelation isChild(FlowWithSource parent, FlowWithSource child) {
|
||||
@VisibleForTesting
|
||||
public FlowRelation isChild(Flow parent, Flow child) {
|
||||
if (this.isFlowTaskChild(parent, child)) {
|
||||
return FlowRelation.FLOW_TASK;
|
||||
}
|
||||
@@ -152,7 +156,7 @@ public class FlowTopologyService {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean isFlowTaskChild(FlowWithSource parent, FlowWithSource child) {
|
||||
protected boolean isFlowTaskChild(Flow parent, Flow child) {
|
||||
try {
|
||||
return parent
|
||||
.allTasksWithChilds()
|
||||
@@ -168,7 +172,7 @@ public class FlowTopologyService {
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isTriggerChild(FlowWithSource parent, FlowWithSource child) {
|
||||
protected boolean isTriggerChild(Flow parent, Flow child) {
|
||||
List<AbstractTrigger> triggers = ListUtils.emptyOnNull(child.getTriggers());
|
||||
|
||||
// simulated execution: we add a "simulated" label so conditions can know that the evaluation is for a simulated execution
|
||||
@@ -196,7 +200,7 @@ public class FlowTopologyService {
|
||||
return conditionMatch && preconditionMatch;
|
||||
}
|
||||
|
||||
private boolean validateCondition(Condition condition, FlowWithSource child, Execution execution) {
|
||||
private boolean validateCondition(Condition condition, FlowInterface child, Execution execution) {
|
||||
if (isFilterCondition(condition)) {
|
||||
return true;
|
||||
}
|
||||
@@ -208,7 +212,7 @@ public class FlowTopologyService {
|
||||
return this.conditionService.isValid(condition, child, execution);
|
||||
}
|
||||
|
||||
private boolean validateMultipleConditions(Map<String, Condition> multipleConditions, FlowWithSource child, Execution execution) {
|
||||
private boolean validateMultipleConditions(Map<String, Condition> multipleConditions, FlowInterface child, Execution execution) {
|
||||
List<Condition> conditions = multipleConditions
|
||||
.values()
|
||||
.stream()
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.kestra.core.trace.propagation;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.opentelemetry.context.propagation.TextMapGetter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
public class ExecutionTextMapGetter implements TextMapGetter<Execution> {
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.kestra.core.trace.propagation;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.opentelemetry.context.propagation.TextMapSetter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
|
||||
public class ExecutionTextMapSetter implements TextMapSetter<Execution> {
|
||||
public static final ExecutionTextMapSetter INSTANCE = new ExecutionTextMapSetter();
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.kestra.core.trace.propagation;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.opentelemetry.context.propagation.TextMapGetter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
public class RunContextTextMapGetter implements TextMapGetter<RunContext> {
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.kestra.core.trace.propagation;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.opentelemetry.context.propagation.TextMapSetter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.annotation.Nullable;
|
||||
|
||||
public class RunContextTextMapSetter implements TextMapSetter<RunContext> {
|
||||
public static final RunContextTextMapSetter INSTANCE = new RunContextTextMapSetter();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user