Compare commits

...

105 Commits

Author SHA1 Message Date
Loïc Mathieu
485f9a3669 feat(jdbc): Improve internal queue cleaning
Instead of cleaning queues via the JdbcCleaner, or via queues.deleteByIds(), directly clean some queues after processing.
We only do this for queues that are known to have a single consumer, for these queues, instead of updating the offsets after consumption, we remove directly the records.
2025-04-07 17:52:05 +02:00
dependabot[bot]
ae7bb88ff0 build(deps): bump com.google.guava:guava from 33.4.0-jre to 33.4.6-jre
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.4.0-jre to 33.4.6-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 17:45:18 +02:00
Florian Hussonnois
8f29a72df7 refactor: add GenericFlow to support un-typed flow deserialization
Add new FlowId, FlowInterface and GenericFlow classes to support
deserialization of flow with un-typed plugins (i.e., tasks, triggers)
in order to inject defaults prior to strongly-typed deserialization.
2025-04-07 17:32:06 +02:00
Loïc Mathieu
fc8732f96e chore: use @Nullable from Jakarata annotations 2025-04-07 17:01:52 +02:00
Loïc Mathieu
14f4449d99 chore(deps): Upgrade Guava to 33.4.5-jre (#8005) 2025-04-07 17:01:52 +02:00
AJ Emerich
dd80a91ab3 fix(docs): remove note about Podman rootless (#8259)
closes https://github.com/kestra-io/docs/issues/2404
2025-04-07 16:54:06 +02:00
Florian Hussonnois
840f010921 fix(core): fix NPE when generating flow graph
Fix NPE when generating flow graph and a task cannot be deserialized

Fix: kestra-io/kestra-ee#3369
2025-04-07 16:33:08 +02:00
Loïc Mathieu
8462b178cb feat(jdbc-h2,jdbc-mysql,jdbc-postgres): add an index on queues.key 2025-04-07 15:55:54 +02:00
brian.mulier
901625786d fix(ui)!: prevent infinite loading loop in Namespace KV Store & Secrets pages if there is none 2025-04-07 15:07:50 +02:00
YannC
4def8c5764 chore(ci): Implement JReleaser for GitHub Release (#8231) 2025-04-07 13:34:33 +02:00
brian.mulier
65a204356c fix(ui): bump ui-libs to 0.0.168 2025-04-07 12:03:06 +02:00
dependabot[bot]
73e3fd08e9 build(deps): bump software.amazon.awssdk:bom from 2.31.11 to 2.31.16
Bumps software.amazon.awssdk:bom from 2.31.11 to 2.31.16.

---
updated-dependencies:
- dependency-name: software.amazon.awssdk:bom
  dependency-version: 2.31.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 11:29:42 +02:00
dependabot[bot]
0665b52014 build(deps): bump org.owasp.dependencycheck from 12.1.0 to 12.1.1
Bumps org.owasp.dependencycheck from 12.1.0 to 12.1.1.

---
updated-dependencies:
- dependency-name: org.owasp.dependencycheck
  dependency-version: 12.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 11:05:05 +02:00
dependabot[bot]
781f9dc8d8 build(deps): bump com.google.cloud:libraries-bom from 26.58.0 to 26.59.0
Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.58.0 to 26.59.0.
- [Release notes](https://github.com/googleapis/java-cloud-bom/releases)
- [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json)
- [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.58.0...v26.59.0)

---
updated-dependencies:
- dependency-name: com.google.cloud:libraries-bom
  dependency-version: 26.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 11:04:49 +02:00
dependabot[bot]
92e4570158 build(deps): bump org.jooq:jooq from 3.20.2 to 3.20.3
Bumps org.jooq:jooq from 3.20.2 to 3.20.3.

---
updated-dependencies:
- dependency-name: org.jooq:jooq
  dependency-version: 3.20.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 11:04:36 +02:00
brian.mulier
ce47b4ee5e fix(ui): bump ui-libs to 0.0.167 2025-04-07 09:54:35 +02:00
brian.mulier
69193c6096 Revert "fix(core): add additionalProperties=false to tasks to have a warning if there is unknown properties"
This reverts commit 6fee99a78a.
2025-04-07 09:54:30 +02:00
Bart Ledoux
9d437957fa chore: add a rule to prevent angle brackets cast in TS 2025-04-07 09:49:10 +02:00
Ludovic DEHON
dfea86fb07 chore(deps): update pebble to 3.2.4
affected by CVE-2025-1686
2025-04-06 18:23:55 +02:00
brian.mulier
6fee99a78a fix(core): add additionalProperties=false to tasks to have a warning if there is unknown properties 2025-04-05 02:14:20 +02:00
brian.mulier
2c766a5497 feat(webserver): move oneOf to anyOf in JsonSchemaGenerator to have better autocompletion 2025-04-05 01:52:41 +02:00
brian.mulier
959737f545 feat(ui): introduce patched version of monaco-yaml & yaml-language-server to have better autocompletion
waiting for https://github.com/redhat-developer/yaml-language-server/pull/1048
2025-04-05 01:05:27 +02:00
brian.mulier
2c1b6ffe3c fix(ui): exclude additional files from UI coverage report 2025-04-04 16:53:29 +02:00
brian.mulier
9b819f6925 fix(ui): avoid jsx confusion on cast that prevent Storybook from running 2025-04-04 16:24:02 +02:00
brian.mulier
a81c0a5737 fix(ui): move bunch of utils to typescript 2025-04-04 15:38:15 +02:00
brian.mulier
ebcb0bd2a2 feat(ui): add plugin icons to auto-completions + make autocompletion work upon writing full package 2025-04-04 15:28:53 +02:00
Loïc Mathieu
2a26c415bf feat(jdbc): avoid using the WorkerTaskResult queue when possible
Instead, directly process the result (add the taskrun) to avoid one rountrip inside the queue.
2025-04-04 14:15:13 +02:00
Loïc Mathieu
1e06b9f1c0 feat(jdbc): allow disabling queue cleaning. 2025-04-04 13:43:09 +02:00
Loïc Mathieu
0b64da5e84 fix(core): default namespace in namespace file 2025-04-04 11:51:00 +02:00
AJ Emerich
d95b65082f fix(docs): add title, description, and example to Dashboard chart data (#8220)
* fix(docs): add title, description, and example to Metrics chart data

* fix(docs): add title, description, example for Logs and Executions

* fix(docs): fix line break in Assert plugin docs
2025-04-04 08:18:14 +02:00
Mathieu Gabelle
1bba3e4035 refactor: migrate plugin condition to dynamic properties (#6907)
* refactor: migrate plugin condition to dynamic properties
2025-04-04 08:18:01 +02:00
Will Russell
efe0c6e1e4 fix(docs): update task type in example to updated name 2025-04-03 17:19:02 +01:00
Nicolas K.
fce7ec135e fire(core): flaky allow failure for each item test (#8228)
Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-04-03 18:00:15 +02:00
Nicolas K.
5a2456716f fix(jdbc): #8219 unquoted timestamp field breaking query (#8222)
* fix(jdbc): #8219 unquoted timestamp field breaking query

* fix(jdbc): #8219 fix hard coded quoted field

---------

Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-04-03 14:54:05 +02:00
Loïc Mathieu
f43f8c2dc0 fix(core): be tolerant of decryption issue
If we cannot decrupt outputs, let's ignore the outpot and log a warning.
This may only happen on configuratin mismatch between nodes.
2025-04-03 13:57:41 +02:00
YannC
94811d4e06 chore(ci): align plugins handle for docker publish on EE CI 2025-04-03 13:16:41 +02:00
YannC
b7166488be chore(ci): modify publish docker to align on EE 2025-04-03 11:56:11 +02:00
brian.mulier
78066fef62 fix(ui): remove all warnings from MonacoEditor.vue 2025-04-02 19:33:04 +02:00
brian.mulier
c70abcb85f fix(ui): auto-resize suggest window to fit tasks
closes #5823
2025-04-02 17:01:36 +02:00
YannC
774a4ef7a1 Feat(): ci changes (#8217)
* fix(): avoid running release workflow on releases branch

* feat(): avoid running CI on draft PR

close #4964

* fix(ci): only publish docker image in workflow release if develop branch or specific asked

close #8136
2025-04-02 15:51:28 +02:00
Florian Hussonnois
a48fd02ed7 fix(cli): fix NPE for commands not requiring plugins (#8212)
Fix: #8212
2025-04-02 14:51:41 +02:00
Loïc Mathieu
030d948521 chore(core): remove un-used attribute in FlowWithWorkerTrigger 2025-04-02 12:18:23 +02:00
Loïc Mathieu
c4aa6c1097 fix(jdbc): possible deadlock on service instance
If multiple Executors restart at the same time and there was a not of worker task to resubmit, there was a possible deadlock as the service instance table is selected for update so it can block other executors.
Using skipped lock avoid that and is still correct as other executors can skip the dead instance handling as it was already in process by the first executor.

findById was not changed in this commit as it's not part of the worker task resubmission process.
2025-04-02 10:53:54 +02:00
Loïc Mathieu
3d1a3d0e7a fix(ci): use the right GitHub token for test report 2025-04-02 10:53:28 +02:00
Loïc Mathieu
30ab030244 fix(gradle): Windows selfrun.bat
Fixes https://github.com/kestra-io/kestra-ee/issues/3324
2025-04-02 09:39:59 +02:00
brian.mulier
cc083385f0 fix(ui): better autocompletion relevance
closes #7709
2025-04-01 19:33:42 +02:00
Shruti Mantri
c14462f5fa feat: add afterExecution to basic.md (#8126)
* feat: add afterExecution to basic.md

* Update ui/src/assets/docs/basic.md

---------

Co-authored-by: AJ Emerich <aemerich@kestra.io>
2025-04-01 16:42:51 +02:00
Mathieu Gabelle
d6e470d788 fix!: update pullPolicy default value to IF_NOT_PRESENT (#8170) 2025-04-01 11:24:59 +02:00
Loïc Mathieu
58ae507e21 fix(core): mask secrets on log attributes
Fixes #3282
2025-04-01 10:09:11 +02:00
Loïc Mathieu
71110ccfc3 Revert "fix(core): HttpClient log the URL even if it's a secret"
This reverts commit 54aa935702.
2025-04-01 10:09:11 +02:00
YannC
fdcc07b546 fix(): allows namespace to be search by in filter 2025-04-01 10:07:52 +02:00
Mathieu Gabelle
221236e079 fix(scripts)!: update pull policy to IF_NOT_PRESENT (#8169)
BREAKING CHANGE: in accordance with new Docker Hub pull policy regulation, the default kestra pull policy will change from ALWAYS to IF_NOT_PRESENT
2025-04-01 08:12:39 +02:00
Ludovic DEHON
d14deaceb0 fix(core): add a meaningful log for flow that can inject defaults 2025-03-31 18:22:16 +02:00
YannC
bfdc48bbbe fix(cli): prevent FlowUpdatesCommand to crash due to plugin loader 2025-03-31 17:45:32 +02:00
brian.mulier
e6b2f1f79a fix(ui-ee): keep fetching if filtered kvs & secrets have no elements after fetch
might close kestra-io/kestra-ee#3311
2025-03-31 16:12:45 +02:00
Loïc Mathieu
0632052837 fix(core): use a stable flow logger name
If we keep the executionId in it, as it's now used to create the forward logger, a new logger will be created for each execution.
This may also fix a memory leak.
2025-03-31 14:56:13 +02:00
Florian Hussonnois
3df9d49aa0 ci: fix setversion-tag and devtools 2025-03-31 13:50:27 +02:00
Loïc Mathieu
318f2b7d5a chore(cli,jdbc-postgres): fix some compilation warnings 2025-03-31 13:17:35 +02:00
Roman Acevedo
800970a88f chore(deps): add bouncycastle:bcpkix-jdk18on to platform 2025-03-31 11:52:33 +02:00
dependabot[bot]
f717063a83 build(deps): bump org.sonarqube from 6.0.1.5171 to 6.1.0.5360
Bumps org.sonarqube from 6.0.1.5171 to 6.1.0.5360.

---
updated-dependencies:
- dependency-name: org.sonarqube
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 11:20:34 +02:00
Barthélémy Ledoux
9a7fb64943 fix: make flowWarnings show to unlock saving (#8157)
closes #8115
2025-03-31 10:59:46 +02:00
Barthélémy Ledoux
45bddb8d09 fix: task array needed better typings (#8158)
closes #8117
2025-03-31 10:59:32 +02:00
dependabot[bot]
881b009d9b build(deps): bump flyingSaucerVersion from 9.11.4 to 9.11.5
Bumps `flyingSaucerVersion` from 9.11.4 to 9.11.5.

Updates `org.xhtmlrenderer:flying-saucer-core` from 9.11.4 to 9.11.5
- [Release notes](https://github.com/flyingsaucerproject/flyingsaucer/releases)
- [Changelog](https://github.com/flyingsaucerproject/flyingsaucer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flyingsaucerproject/flyingsaucer/compare/v9.11.4...v9.11.5)

Updates `org.xhtmlrenderer:flying-saucer-pdf` from 9.11.4 to 9.11.5
- [Release notes](https://github.com/flyingsaucerproject/flyingsaucer/releases)
- [Changelog](https://github.com/flyingsaucerproject/flyingsaucer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flyingsaucerproject/flyingsaucer/compare/v9.11.4...v9.11.5)

---
updated-dependencies:
- dependency-name: org.xhtmlrenderer:flying-saucer-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.xhtmlrenderer:flying-saucer-pdf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 10:46:21 +02:00
dependabot[bot]
ab818713f6 build(deps): bump software.amazon.awssdk:bom from 2.31.6 to 2.31.11
Bumps software.amazon.awssdk:bom from 2.31.6 to 2.31.11.

---
updated-dependencies:
- dependency-name: software.amazon.awssdk:bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 10:27:54 +02:00
Loïc Mathieu
d68ffa3109 chore: disable tests that are too flaky
An issue will be open to track them and re-enabled them later.
2025-03-31 10:27:46 +02:00
dependabot[bot]
addd76f9bb build(deps): bump com.azure:azure-sdk-bom from 1.2.32 to 1.2.33
Bumps [com.azure:azure-sdk-bom](https://github.com/azure/azure-sdk-for-java) from 1.2.32 to 1.2.33.
- [Release notes](https://github.com/azure/azure-sdk-for-java/releases)
- [Commits](https://github.com/azure/azure-sdk-for-java/compare/azure-sdk-bom_1.2.32...azure-sdk-bom_1.2.33)

---
updated-dependencies:
- dependency-name: com.azure:azure-sdk-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 09:58:43 +02:00
dependabot[bot]
6be939c1bd build(deps): bump software.amazon.awssdk.crt:aws-crt
Bumps [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java) from 0.36.3 to 0.37.0.
- [Release notes](https://github.com/awslabs/aws-crt-java/releases)
- [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.36.3...v0.37.0)

---
updated-dependencies:
- dependency-name: software.amazon.awssdk.crt:aws-crt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 09:44:07 +02:00
dependabot[bot]
4ea876d3fe build(deps): bump com.google.cloud:libraries-bom from 26.57.0 to 26.58.0
Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.57.0 to 26.58.0.
- [Release notes](https://github.com/googleapis/java-cloud-bom/releases)
- [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json)
- [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.57.0...v26.58.0)

---
updated-dependencies:
- dependency-name: com.google.cloud:libraries-bom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 09:43:26 +02:00
Miloš Paunović
42d8005eff chore(ui): show blueprint id field in case of missing title (#8154) 2025-03-31 09:40:20 +02:00
Nicolas K.
58a360fae0 feat(core-ee): #7501 split file log exporter to multiple files (#8138)
Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-03-28 17:42:41 +01:00
Loïc Mathieu
49b647e1fc fix(core): add missing docker plugin subgroup icon 2025-03-28 16:15:41 +01:00
Florian Hussonnois
b314fc393b fix(cli): properly register plugins uninstall cmd 2025-03-28 15:40:09 +01:00
Florian Hussonnois
0a298cad17 fix(core): allow dash in plugin version qualifier 2025-03-28 15:18:37 +01:00
Shruti Mantri
61170e6067 feat: add example for HasRetryAttempt condition (#8133) 2025-03-28 14:14:42 +00:00
brian.mulier
cfbffad31a fix(ui): search bars are properly working in secrets & KV pages
closes #8110
closes kestra-io/kestra-ee#3290
2025-03-28 15:02:02 +01:00
Miloš Paunović
41e2dac4ca chore(ui): add padlock icon to secrets menu item (#8129) 2025-03-28 12:44:28 +01:00
Miloš Paunović
0fa8386cb3 chore(ui): amend file tree context menu link colors (#8123) 2025-03-28 12:10:08 +01:00
github-actions[bot]
0f45c009ab chore(translations): localize to languages other than English (#8120) 2025-03-28 11:27:25 +01:00
MilosPaunovic
b86177f329 chore(translations): uniform translation keys with entrerprise edition 2025-03-28 11:21:08 +01:00
brian.mulier
fe396c455b fix(ui): global secret page design
closes kestra-io/kestra-ee#3268
2025-03-28 11:18:34 +01:00
brian.mulier
0830e11645 fix(ui): repair tenant translation 2025-03-28 11:18:34 +01:00
brian.mulier
4d7f6b2bb1 fix(ui): add routeContext where it was missing 2025-03-28 11:18:34 +01:00
brian.mulier
955c6b728b fix(webserver): handle out-of-bounds (>) namespaces fetch 2025-03-28 11:18:34 +01:00
brian.mulier
d2d26351bd fix(core): namespace service now properly detects namespaces with flows inside 2025-03-28 11:18:34 +01:00
Loïc Mathieu
f14b638f73 fix(core): compilation issue 2025-03-28 09:26:01 +01:00
Loïc Mathieu
259b5b5282 Revert "fix(core): require condition in Flow trigger (#7494)"
This reverts commit c5767fd313.
2025-03-28 09:12:40 +01:00
Miloš Paunović
b1c50374b4 chore(ui): handle editor blueprint loading problem (#8113) 2025-03-28 08:57:24 +01:00
Roman Acevedo
de2d923bd4 fix: doc and deprecated field was not showing for dynamic non-string properties (#8006)
fix: doc and deprecated field was not showing for dynamic non-string properties (#8006)
2025-03-27 18:33:23 +01:00
Florian Hussonnois
89c76208a4 fix(core): avoid flow validation error on plugin alias duplicates 2025-03-27 17:59:04 +01:00
YannC
12eb8367ec feat(): add new crudeventtype "account_locked" (#8103) 2025-03-27 17:08:04 +01:00
Nicolas K.
7421f1e93d fix(kafka runner): #2709 filter child forEach tasks before merging th… (#8095)
* fix(kafka runner): #2709 filter child forEach tasks before merging the output, and add sleep before restarting flows to ensure failure is persisted

* feat(kafka runner): #2709 wait until executions are persisted as Failed in the database before restarting

* fix(runner): put back the sleep instead of the wait

* clean(runner): remove unused variables

---------

Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-03-27 16:56:37 +01:00
Loïc Mathieu
5642a53893 fix(core): properly fix the issue with MapUtils.flattenToNestedMap 2025-03-27 16:28:11 +01:00
Loïc Mathieu
d5a2f4430f fix(core): HttpClient log the URL even if it's a secret
Fixes https://github.com/kestra-io/kestra/issues/8092
2025-03-27 16:00:43 +01:00
rajatsingh23
0299e0d5ce chore(ui): Hide duration switch on empty flow dashboard (#8083)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-03-27 15:48:01 +01:00
Barthélémy Ledoux
c69ac99a7f refactor: remove dead code from monacoeditor (#8088)
* refactor(MonacoEditor): improve editor state management and type safety after refactor

* fix: make theme proper valid theme

* fix only types

* remove getEditor

* more fixes

* add vuex store types in options API

* update vuex types

* fix types

* remove unused mapping

* update vuex shims

* fix types
2025-03-27 14:08:59 +01:00
brian.mulier
fd1b4d5234 fix(ui): properly detect yaml to inject json schema into MonacoEditor
closes #8090
2025-03-27 13:39:00 +01:00
Miloš Paunović
4ef3600954 chore(ui): pass custom height property to execution output debug editors (#8100) 2025-03-27 13:36:29 +01:00
Miloš Paunović
366df0f37f chore(ui): include labels of saved search filter on page reload (#8099) 2025-03-27 13:17:11 +01:00
brian.mulier
5e87655a0e fix(webserver): first eval without masking secret function to error in case of missing secret
closes #8094
2025-03-27 13:09:31 +01:00
Miloš Paunović
0f01699d27 chore(ui): make app & dashboard editors re-sizable (#8096) 2025-03-27 12:01:40 +01:00
Loïc Mathieu
0794b2bf8e fix(core): flatten map should not throw an exception
As it is called inside the Executor, it must be fail-safe.
2025-03-27 10:57:32 +01:00
Loïc Mathieu
0becaa0b97 fix(core): charset should not be taken from
Fixes #8072
2025-03-27 10:02:23 +01:00
AJ Emerich
7d3bb34fd4 feat(docs): add kestra.environment and kestra.url expressions (#8085)
https://github.com/kestra-io/kestra-ee/issues/3095
2025-03-27 09:53:24 +01:00
Florian Hussonnois
b8c55baff1 fix(cli): make worker args available through static KestraContext
part-of: kestra-io/kestra-ee#3259
2025-03-26 15:53:51 +01:00
Loïc Mathieu
1f8e5ad18e feat(*): add new methods findAllAsync for the backup 2025-03-26 14:04:01 +01:00
321 changed files with 5449 additions and 2826 deletions

View File

@@ -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' }}

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,4 +87,9 @@ public class FlowUpdatesCommand extends AbstractApiCommand {
return 0;
}
@Override
protected boolean loadExternalPlugins() {
return false;
}
}

View File

@@ -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();
}
);
}

View File

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

View File

@@ -12,6 +12,7 @@ import picocli.CommandLine.Command;
mixinStandardHelpOptions = true,
subcommands = {
PluginInstallCommand.class,
PluginUninstallCommand.class,
PluginListCommand.class,
PluginDocCommand.class,
PluginSearchCommand.class

View File

@@ -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

View File

@@ -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) {

View File

@@ -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");
}

View File

@@ -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).");
}

View File

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

View File

@@ -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()) {

View File

@@ -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");
}
}

View File

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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -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() {

View File

@@ -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) {
}

View File

@@ -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;
}

View File

@@ -8,6 +8,7 @@ public enum CrudEventType {
LOGIN,
LOGOUT,
IMPERSONATE,
LOGIN_FAILURE
LOGIN_FAILURE,
ACCOUNT_LOCKED
}

View File

@@ -23,4 +23,5 @@ public class KestraRuntimeException extends RuntimeException {
public KestraRuntimeException(Throwable cause) {
super(cause);
}
}

View File

@@ -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();
}

View File

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

View File

@@ -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();
}

View File

@@ -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") {

View File

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

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

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

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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())

View File

@@ -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();
}
}

View 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());
}
}

View File

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

View File

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

View File

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

View File

@@ -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."

View File

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

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import javax.annotation.Nullable;
import jakarta.annotation.Nullable;
@AllArgsConstructor
@Getter

View File

@@ -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())

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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(),

View File

@@ -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";

View File

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

View File

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

View File

@@ -94,6 +94,7 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
boolean allowDeleted
);
Flux<Execution> findAllAsync(@Nullable String tenantId);
ArrayListTotal<TaskRun> findTaskRun(
Pageable pageable,

View File

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

View File

@@ -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());
}
}

View File

@@ -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,

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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
) {

View File

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

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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() {

View File

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

View File

@@ -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;
}

View File

@@ -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 -> {

View File

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

View File

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

View File

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

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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() } :

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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> {

View File

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

View File

@@ -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> {

View File

@@ -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