mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
198 Commits
dependabot
...
v1.0.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69b1921236 | ||
|
|
4e99a253e3 | ||
|
|
97d0a93e01 | ||
|
|
e2d8d51843 | ||
|
|
8567ff5490 | ||
|
|
050e22dd09 | ||
|
|
3552eeefbb | ||
|
|
2e47fb8285 | ||
|
|
b52a07e562 | ||
|
|
3f7c01db41 | ||
|
|
f5dbec96e0 | ||
|
|
fe7a6d9af9 | ||
|
|
06c8c35061 | ||
|
|
8f23e813f2 | ||
|
|
47b7c7cd2e | ||
|
|
aca7c2f694 | ||
|
|
a0f29b7d5d | ||
|
|
0176c8c101 | ||
|
|
b0036bbfca | ||
|
|
fad5edbde8 | ||
|
|
f125f63ae5 | ||
|
|
6db1bfb2ce | ||
|
|
0957e07c78 | ||
|
|
5a4a5e44df | ||
|
|
faee3f1827 | ||
|
|
3604762da0 | ||
|
|
6ceb0de1d5 | ||
|
|
4a62f9c818 | ||
|
|
d14f3e3317 | ||
|
|
7e9030dfcf | ||
|
|
2fce17a8a9 | ||
|
|
67d8509106 | ||
|
|
01e92a6d79 | ||
|
|
883b7c8610 | ||
|
|
11ef823567 | ||
|
|
771cca1441 | ||
|
|
53e8674dfc | ||
|
|
59016ae1af | ||
|
|
7503d6fa21 | ||
|
|
0234a4c64c | ||
|
|
98c9c4d21f | ||
|
|
8e54183a44 | ||
|
|
8aa332c629 | ||
|
|
d10893ca00 | ||
|
|
c5ef356a1c | ||
|
|
0313e8e49b | ||
|
|
f4b6161f14 | ||
|
|
e69e82a35e | ||
|
|
e77378bcb7 | ||
|
|
3c9df90a35 | ||
|
|
6c86f0917c | ||
|
|
30b7346ee0 | ||
|
|
2f485c74ff | ||
|
|
3a5713bbd1 | ||
|
|
2eed738b83 | ||
|
|
5e2609ce5e | ||
|
|
86f909ce93 | ||
|
|
a8cb28a127 | ||
|
|
0fe9ba3e13 | ||
|
|
40f5aadd1a | ||
|
|
ceac25429a | ||
|
|
4144d9fbb1 | ||
|
|
9cc7d45f74 | ||
|
|
81ee330b9e | ||
|
|
5382655a2e | ||
|
|
483f7dc3b2 | ||
|
|
3c2da63837 | ||
|
|
31527891b2 | ||
|
|
6364f419d9 | ||
|
|
3c14432412 | ||
|
|
eaea4f5012 | ||
|
|
d43390a579 | ||
|
|
2404c36d35 | ||
|
|
bdbd217171 | ||
|
|
019c16af3c | ||
|
|
ff7d7c6a0b | ||
|
|
1042be87da | ||
|
|
104805d780 | ||
|
|
33c8e54f36 | ||
|
|
ff2e00d1ca | ||
|
|
0fe3f317c7 | ||
|
|
f753d15c91 | ||
|
|
c03e31de68 | ||
|
|
9a79f9a64c | ||
|
|
41468652d4 | ||
|
|
bc182277de | ||
|
|
8c2271089c | ||
|
|
9973a2120b | ||
|
|
bdfd038d40 | ||
|
|
a3fd734082 | ||
|
|
553a1d5389 | ||
|
|
c58aca967b | ||
|
|
27dcf60770 | ||
|
|
4e7c75232a | ||
|
|
f452da7ce1 | ||
|
|
43401c5017 | ||
|
|
067b110cf0 | ||
|
|
4ceff83a28 | ||
|
|
5026afe5bf | ||
|
|
3c899fcb2f | ||
|
|
cee412ffa9 | ||
|
|
3a57a683be | ||
|
|
a0b9de934e | ||
|
|
d677317cc5 | ||
|
|
9e661195e5 | ||
|
|
09c921bee5 | ||
|
|
d21ec4e899 | ||
|
|
efdb25fa97 | ||
|
|
37bdcc342c | ||
|
|
6d35f2b7a6 | ||
|
|
fe46ddf381 | ||
|
|
359dc9adc0 | ||
|
|
39c930124f | ||
|
|
1686fc3b4e | ||
|
|
03ff25ff55 | ||
|
|
d02fd53287 | ||
|
|
6c16bbe853 | ||
|
|
aa7a473d49 | ||
|
|
95133ebc40 | ||
|
|
54482e1d06 | ||
|
|
54b7811812 | ||
|
|
050ad60a95 | ||
|
|
030627ba7b | ||
|
|
c06ef7958f | ||
|
|
692d046289 | ||
|
|
92c1f04ec0 | ||
|
|
9e11d5fe5e | ||
|
|
14952c9457 | ||
|
|
ae314c301d | ||
|
|
f8aa5fb6ba | ||
|
|
c87d7e4da0 | ||
|
|
c928f1d822 | ||
|
|
baa07dd02b | ||
|
|
260cb50651 | ||
|
|
0a45325c69 | ||
|
|
c2522e2544 | ||
|
|
27476279ae | ||
|
|
3cc6372cb5 | ||
|
|
5f6e9dbe06 | ||
|
|
5078ce741d | ||
|
|
b7e17b7114 | ||
|
|
acaee34b0e | ||
|
|
1d78332505 | ||
|
|
7249632510 | ||
|
|
4a66a08c3b | ||
|
|
22fd6e97ea | ||
|
|
9afd86d32b | ||
|
|
797ea6c9e4 | ||
|
|
07d5e815c4 | ||
|
|
33ac9b1495 | ||
|
|
4d5b95d040 | ||
|
|
667aca7345 | ||
|
|
e05cc65202 | ||
|
|
71b606c27c | ||
|
|
47f9f12ce8 | ||
|
|
01acae5e97 | ||
|
|
e5878f08b7 | ||
|
|
0bcb6b4e0d | ||
|
|
3c2ecf4342 | ||
|
|
3d4f66772e | ||
|
|
e2afd4bcc3 | ||
|
|
d143097f03 | ||
|
|
72c0d91c1a | ||
|
|
1d692e56b0 | ||
|
|
0352d617ac | ||
|
|
b41aa4e0b9 | ||
|
|
d811dc030b | ||
|
|
105e62eee1 | ||
|
|
28796862a4 | ||
|
|
637cd794a4 | ||
|
|
fdd5c6e63d | ||
|
|
eda2483ec9 | ||
|
|
7b3c296489 | ||
|
|
fe6f8b4ed9 | ||
|
|
17ff539690 | ||
|
|
bbd0dda47e | ||
|
|
27a8e8b5a7 | ||
|
|
d6620a34cd | ||
|
|
6f8b3c5cfd | ||
|
|
6da6cbab60 | ||
|
|
a899e16178 | ||
|
|
568cd0b0c7 | ||
|
|
92e1dcb6eb | ||
|
|
499e040cd0 | ||
|
|
5916831d62 | ||
|
|
0b1b55957e | ||
|
|
7ee40d376a | ||
|
|
e2c9b3e256 | ||
|
|
556730777b | ||
|
|
c1a75a431f | ||
|
|
4a5b91667a | ||
|
|
f7b2af16a1 | ||
|
|
9351cb22e0 | ||
|
|
b1ecb82fdc | ||
|
|
c6d56151eb | ||
|
|
ed4398467a | ||
|
|
c51947419a | ||
|
|
ccb6a1f4a7 |
29
.github/actions/plugins-list/action.yml
vendored
29
.github/actions/plugins-list/action.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: 'Load Kestra Plugin List'
|
|
||||||
description: 'Composite action to load list of plugins'
|
|
||||||
inputs:
|
|
||||||
plugin-version:
|
|
||||||
description: "Kestra version"
|
|
||||||
default: 'LATEST'
|
|
||||||
required: true
|
|
||||||
plugin-file:
|
|
||||||
description: "File of the plugins"
|
|
||||||
default: './.plugins'
|
|
||||||
required: true
|
|
||||||
outputs:
|
|
||||||
plugins:
|
|
||||||
description: "List of all Kestra plugins"
|
|
||||||
value: ${{ steps.plugins.outputs.plugins }}
|
|
||||||
repositories:
|
|
||||||
description: "List of all Kestra repositories of plugins"
|
|
||||||
value: ${{ steps.plugins.outputs.repositories }}
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Get Plugins List
|
|
||||||
id: plugins
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
PLUGINS=$([ -f ${{ inputs.plugin-file }} ] && cat ${{ inputs.plugin-file }} | grep "io\\.kestra\\." | sed -e '/#/s/^.//' | sed -e "s/LATEST/${{ inputs.plugin-version }}/g" | cut -d':' -f2- | xargs || echo '');
|
|
||||||
REPOSITORIES=$([ -f ${{ inputs.plugin-file }} ] && cat ${{ inputs.plugin-file }} | grep "io\\.kestra\\." | sed -e '/#/s/^.//' | cut -d':' -f1 | uniq | sort | xargs || echo '')
|
|
||||||
echo "plugins=$PLUGINS" >> $GITHUB_OUTPUT
|
|
||||||
echo "repositories=$REPOSITORIES" >> $GITHUB_OUTPUT
|
|
||||||
20
.github/actions/setup-vars/action.yml
vendored
20
.github/actions/setup-vars/action.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: 'Setup vars'
|
|
||||||
description: 'Composite action to setup common vars'
|
|
||||||
outputs:
|
|
||||||
tag:
|
|
||||||
description: "Git tag"
|
|
||||||
value: ${{ steps.vars.outputs.tag }}
|
|
||||||
commit:
|
|
||||||
description: "Git commit"
|
|
||||||
value: ${{ steps.vars.outputs.commit }}
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
# Setup vars
|
|
||||||
- name: Set variables
|
|
||||||
id: vars
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
TAG=${GITHUB_REF#refs/*/}
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
echo "commit=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_OUTPUT
|
|
||||||
67
.github/workflows/auto-translate-ui-keys.yml
vendored
67
.github/workflows/auto-translate-ui-keys.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: Auto-Translate UI keys and create PR
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 9-21/3 * * *" # Every 3 hours from 9 AM to 9 PM
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
retranslate_modified_keys:
|
|
||||||
description: "Whether to re-translate modified keys even if they already have translations."
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "false"
|
|
||||||
- "true"
|
|
||||||
default: "false"
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
translations:
|
|
||||||
name: Translations
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
name: Checkout
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: pip install gitpython openai
|
|
||||||
|
|
||||||
- name: Generate translations
|
|
||||||
run: python ui/src/translations/generate_translations.py ${{ github.event.inputs.retranslate_modified_keys }}
|
|
||||||
env:
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20.x"
|
|
||||||
|
|
||||||
- name: Set up Git
|
|
||||||
run: |
|
|
||||||
git config --global user.name "GitHub Action"
|
|
||||||
git config --global user.email "actions@github.com"
|
|
||||||
|
|
||||||
- name: Commit and create PR
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
BRANCH_NAME="chore/update-translations-$(date +%s)"
|
|
||||||
git checkout -b $BRANCH_NAME
|
|
||||||
git add ui/src/translations/*.json
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No changes to commit. Exiting with success."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
git commit -m "chore(core): localize to languages other than english" -m "Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference."
|
|
||||||
git push -u origin $BRANCH_NAME || (git push origin --delete $BRANCH_NAME && git push -u origin $BRANCH_NAME)
|
|
||||||
gh pr create --title "Translations from en.json" --body $'This PR was created automatically by a GitHub Action.\n\nSomeone from the @kestra-io/frontend team needs to review and merge.' --base ${{ github.ref_name }} --head $BRANCH_NAME
|
|
||||||
|
|
||||||
- name: Check keys matching
|
|
||||||
run: node ui/src/translations/check.js
|
|
||||||
85
.github/workflows/codeql-analysis.yml
vendored
85
.github/workflows/codeql-analysis.yml
vendored
@@ -1,85 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 5 * * 1'
|
|
||||||
|
|
||||||
workflow_dispatch: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# Override automatic language detection by changing the below list
|
|
||||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
|
||||||
language: ['java', 'javascript']
|
|
||||||
# Learn more...
|
|
||||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
# We must fetch at least the immediate parents so that if this is
|
|
||||||
# a pull request then we can checkout the head.
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
# If this run was triggered by a pull request event, then checkout
|
|
||||||
# the head of the pull request instead of the merge commit.
|
|
||||||
- run: git checkout HEAD^2
|
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Set up JDK
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
if: ${{ matrix.language == 'java' }}
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: 21
|
|
||||||
|
|
||||||
- name: Setup gradle
|
|
||||||
if: ${{ matrix.language == 'java' }}
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Build with Gradle
|
|
||||||
if: ${{ matrix.language == 'java' }}
|
|
||||||
run: ./gradlew testClasses -x :ui:assembleFrontend
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
if: ${{ matrix.language != 'java' }}
|
|
||||||
uses: github/codeql-action/autobuild@v3
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v3
|
|
||||||
86
.github/workflows/e2e.yml
vendored
86
.github/workflows/e2e.yml
vendored
@@ -1,86 +0,0 @@
|
|||||||
name: 'E2E tests revival'
|
|
||||||
description: 'New E2E tests implementation started by Roman. Based on playwright in npm UI project, tests Kestra OSS develop docker image. These tests are written from zero, lets make them unflaky from the start!.'
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 * * * *" # Every hour
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
noInputYet:
|
|
||||||
description: 'not input yet.'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: "no input"
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
noInputYet:
|
|
||||||
description: 'not input yet.'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: "no input"
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
timeout-minutes: 15
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
|
||||||
steps:
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Checkout kestra
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
path: kestra
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: kestra-io/actions/.github/actions/setup-build@main
|
|
||||||
name: Setup - Build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: true
|
|
||||||
node-enabled: true
|
|
||||||
python-enabled: true
|
|
||||||
|
|
||||||
- name: Install Npm dependencies
|
|
||||||
run: |
|
|
||||||
cd kestra/ui
|
|
||||||
npm i
|
|
||||||
npx playwright install --with-deps chromium
|
|
||||||
|
|
||||||
- name: Run E2E Tests
|
|
||||||
run: |
|
|
||||||
cd kestra
|
|
||||||
sh build-and-start-e2e-tests.sh
|
|
||||||
|
|
||||||
- name: Upload Playwright Report as Github artifact
|
|
||||||
# 'With this report, you can analyze locally the results of the tests. see https://playwright.dev/docs/ci-intro#html-report'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: kestra/ui/playwright-report/
|
|
||||||
retention-days: 7
|
|
||||||
# Allure check
|
|
||||||
# TODO I don't know what it should do
|
|
||||||
# - uses: rlespinasse/github-slug-action@v5
|
|
||||||
# name: Allure - Generate slug variables
|
|
||||||
#
|
|
||||||
# - name: Allure - Publish report
|
|
||||||
# uses: andrcuns/allure-publish-action@v2.9.0
|
|
||||||
# if: always() && env.GOOGLE_SERVICE_ACCOUNT != ''
|
|
||||||
# continue-on-error: true
|
|
||||||
# env:
|
|
||||||
# GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
|
||||||
# JAVA_HOME: /usr/lib/jvm/default-jvm/
|
|
||||||
# with:
|
|
||||||
# storageType: gcs
|
|
||||||
# resultsGlob: "**/build/allure-results"
|
|
||||||
# bucket: internal-kestra-host
|
|
||||||
# baseUrl: "https://internal.dev.kestra.io"
|
|
||||||
# prefix: ${{ format('{0}/{1}', github.repository, 'allure/java') }}
|
|
||||||
# copyLatest: true
|
|
||||||
# ignoreMissingResults: true
|
|
||||||
82
.github/workflows/gradle-release-plugins.yml
vendored
82
.github/workflows/gradle-release-plugins.yml
vendored
@@ -1,82 +0,0 @@
|
|||||||
name: Run Gradle Release for Kestra Plugins
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
releaseVersion:
|
|
||||||
description: 'The release version (e.g., 0.21.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
nextVersion:
|
|
||||||
description: 'The next version (e.g., 0.22.0-SNAPSHOT)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
dryRun:
|
|
||||||
description: 'Use DRY_RUN mode'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release plugins
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Checkout
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Checkout GitHub Actions
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
repository: kestra-io/actions
|
|
||||||
path: actions
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: ./actions/.github/actions/setup-build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: true
|
|
||||||
node-enabled: true
|
|
||||||
python-enabled: true
|
|
||||||
caches-enabled: true
|
|
||||||
|
|
||||||
# Get Plugins List
|
|
||||||
- name: Get Plugins List
|
|
||||||
uses: ./.github/actions/plugins-list
|
|
||||||
id: plugins-list
|
|
||||||
with:
|
|
||||||
plugin-version: 'LATEST'
|
|
||||||
|
|
||||||
- name: 'Configure Git'
|
|
||||||
run: |
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
- name: Run Gradle Release
|
|
||||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
|
||||||
env:
|
|
||||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
run: |
|
|
||||||
chmod +x ./dev-tools/release-plugins.sh;
|
|
||||||
|
|
||||||
./dev-tools/release-plugins.sh \
|
|
||||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
|
||||||
--next-version=${{github.event.inputs.nextVersion}} \
|
|
||||||
--yes \
|
|
||||||
${{ steps.plugins-list.outputs.repositories }}
|
|
||||||
|
|
||||||
- name: Run Gradle Release (DRY_RUN)
|
|
||||||
if: ${{ github.event.inputs.dryRun == 'true' }}
|
|
||||||
env:
|
|
||||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
run: |
|
|
||||||
chmod +x ./dev-tools/release-plugins.sh;
|
|
||||||
|
|
||||||
./dev-tools/release-plugins.sh \
|
|
||||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
|
||||||
--next-version=${{github.event.inputs.nextVersion}} \
|
|
||||||
--dry-run \
|
|
||||||
--yes \
|
|
||||||
${{ steps.plugins-list.outputs.repositories }}
|
|
||||||
91
.github/workflows/gradle-release.yml
vendored
91
.github/workflows/gradle-release.yml
vendored
@@ -1,91 +0,0 @@
|
|||||||
name: Run Gradle Release
|
|
||||||
run-name: "Releasing Kestra ${{ github.event.inputs.releaseVersion }} 🚀"
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
releaseVersion:
|
|
||||||
description: 'The release version (e.g., 0.21.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
nextVersion:
|
|
||||||
description: 'The next version (e.g., 0.22.0-SNAPSHOT)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
env:
|
|
||||||
RELEASE_VERSION: "${{ github.event.inputs.releaseVersion }}"
|
|
||||||
NEXT_VERSION: "${{ github.event.inputs.nextVersion }}"
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release Kestra
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/develop'
|
|
||||||
steps:
|
|
||||||
# Checks
|
|
||||||
- name: Check Inputs
|
|
||||||
run: |
|
|
||||||
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+)\.0$ ]]; then
|
|
||||||
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)\.0$"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! [[ "$NEXT_VERSION" =~ ^[0-9]+(\.[0-9]+)\.0-SNAPSHOT$ ]]; then
|
|
||||||
echo "Invalid next version. Must match regex: ^[0-9]+(\.[0-9]+)\.0-SNAPSHOT$"
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
# Checkout
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
path: kestra
|
|
||||||
|
|
||||||
# Checkout GitHub Actions
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
repository: kestra-io/actions
|
|
||||||
path: actions
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: ./actions/.github/actions/setup-build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: true
|
|
||||||
node-enabled: true
|
|
||||||
python-enabled: true
|
|
||||||
caches-enabled: true
|
|
||||||
|
|
||||||
- name: Configure Git
|
|
||||||
run: |
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
- name: Run Gradle Release
|
|
||||||
env:
|
|
||||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Extract the major and minor versions
|
|
||||||
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
|
||||||
PUSH_RELEASE_BRANCH="releases/v${BASE_VERSION}.x"
|
|
||||||
|
|
||||||
cd kestra
|
|
||||||
|
|
||||||
# Create and push release branch
|
|
||||||
git checkout -b "$PUSH_RELEASE_BRANCH";
|
|
||||||
git push -u origin "$PUSH_RELEASE_BRANCH";
|
|
||||||
|
|
||||||
# Run gradle release
|
|
||||||
git checkout develop;
|
|
||||||
|
|
||||||
if [[ "$RELEASE_VERSION" == *"-SNAPSHOT" ]]; then
|
|
||||||
./gradlew release -Prelease.useAutomaticVersion=true \
|
|
||||||
-Prelease.releaseVersion="${RELEASE_VERSION}" \
|
|
||||||
-Prelease.newVersion="${NEXT_VERSION}" \
|
|
||||||
-Prelease.pushReleaseVersionBranch="${PUSH_RELEASE_BRANCH}" \
|
|
||||||
-Prelease.failOnSnapshotDependencies=false
|
|
||||||
else
|
|
||||||
./gradlew release -Prelease.useAutomaticVersion=true \
|
|
||||||
-Prelease.releaseVersion="${RELEASE_VERSION}" \
|
|
||||||
-Prelease.newVersion="${NEXT_VERSION}" \
|
|
||||||
-Prelease.pushReleaseVersionBranch="${PUSH_RELEASE_BRANCH}"
|
|
||||||
fi
|
|
||||||
86
.github/workflows/main-build.yml
vendored
Normal file
86
.github/workflows/main-build.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: Main Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- releases/*
|
||||||
|
- develop
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
skip-test:
|
||||||
|
description: 'Skip test'
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
default: 'false'
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-main
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-tests:
|
||||||
|
name: Backend tests
|
||||||
|
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-backend-tests.yml@main
|
||||||
|
secrets:
|
||||||
|
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
||||||
|
|
||||||
|
frontend-tests:
|
||||||
|
name: Frontend tests
|
||||||
|
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-frontend-tests.yml@main
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
publish-develop-docker:
|
||||||
|
name: Publish Docker
|
||||||
|
needs: [backend-tests, frontend-tests]
|
||||||
|
if: "!failure() && !cancelled() && github.ref == 'refs/heads/develop'"
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-docker.yml@main
|
||||||
|
with:
|
||||||
|
plugin-version: 'LATEST-SNAPSHOT'
|
||||||
|
secrets:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
|
||||||
|
publish-develop-maven:
|
||||||
|
name: Publish develop Maven
|
||||||
|
needs: [ backend-tests, frontend-tests ]
|
||||||
|
if: "!failure() && !cancelled() && github.ref == 'refs/heads/develop'"
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-maven.yml@main
|
||||||
|
secrets:
|
||||||
|
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
|
||||||
|
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
|
||||||
|
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
||||||
|
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
||||||
|
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
||||||
|
|
||||||
|
end:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [publish-develop-docker, publish-develop-maven]
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Trigger EE Workflow
|
||||||
|
uses: peter-evans/repository-dispatch@v3
|
||||||
|
if: github.ref == 'refs/heads/develop' && needs.release.result == 'success'
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
|
repository: kestra-io/kestra-ee
|
||||||
|
event-type: "oss-updated"
|
||||||
|
|
||||||
|
# Slack
|
||||||
|
- name: Slack - Notification
|
||||||
|
if: ${{ failure() && env.SLACK_WEBHOOK_URL != 0 && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
|
||||||
|
uses: kestra-io/actions/composite/slack-status@main
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||||
83
.github/workflows/main.yml
vendored
83
.github/workflows/main.yml
vendored
@@ -1,83 +0,0 @@
|
|||||||
name: Main Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
skip-test:
|
|
||||||
description: 'Skip test'
|
|
||||||
type: choice
|
|
||||||
required: true
|
|
||||||
default: 'false'
|
|
||||||
options:
|
|
||||||
- "true"
|
|
||||||
- "false"
|
|
||||||
plugin-version:
|
|
||||||
description: "plugins version"
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- main
|
|
||||||
- releases/*
|
|
||||||
- develop
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-main
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
name: Execute tests
|
|
||||||
uses: ./.github/workflows/workflow-test.yml
|
|
||||||
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
|
||||||
with:
|
|
||||||
report-status: false
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
needs: [tests]
|
|
||||||
if: "!failure() && !cancelled() && !startsWith(github.ref, 'refs/heads/releases')"
|
|
||||||
uses: ./.github/workflows/workflow-release.yml
|
|
||||||
with:
|
|
||||||
plugin-version: ${{ inputs.plugin-version != '' && inputs.plugin-version || (github.ref == 'refs/heads/develop' && 'LATEST-SNAPSHOT' || 'LATEST') }}
|
|
||||||
secrets:
|
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
|
|
||||||
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
|
|
||||||
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
|
||||||
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
|
||||||
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
|
||||||
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
|
||||||
|
|
||||||
end:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- release
|
|
||||||
if: always()
|
|
||||||
env:
|
|
||||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
||||||
steps:
|
|
||||||
- name: Trigger EE Workflow
|
|
||||||
uses: peter-evans/repository-dispatch@v3
|
|
||||||
if: github.ref == 'refs/heads/develop' && needs.release.result == 'success'
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
repository: kestra-io/kestra-ee
|
|
||||||
event-type: "oss-updated"
|
|
||||||
|
|
||||||
|
|
||||||
# Slack
|
|
||||||
- name: Slack - Notification
|
|
||||||
uses: Gamesight/slack-workflow-status@master
|
|
||||||
if: ${{ always() && env.SLACK_WEBHOOK_URL != 0 }}
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
||||||
name: GitHub Actions
|
|
||||||
icon_emoji: ":github-actions:"
|
|
||||||
channel: "C02DQ1A7JLR" # _int_git channel
|
|
||||||
60
.github/workflows/pre-release.yml
vendored
Normal file
60
.github/workflows/pre-release.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Pre Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
skip-test:
|
||||||
|
description: 'Skip test'
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
default: 'false'
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-artifacts:
|
||||||
|
name: Build Artifacts
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-build-artifacts.yml@main
|
||||||
|
|
||||||
|
backend-tests:
|
||||||
|
name: Backend tests
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-backend-tests.yml@main
|
||||||
|
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
||||||
|
secrets:
|
||||||
|
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
||||||
|
|
||||||
|
frontend-tests:
|
||||||
|
name: Frontend tests
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-frontend-tests.yml@main
|
||||||
|
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
||||||
|
secrets:
|
||||||
|
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
publish-maven:
|
||||||
|
name: Publish Maven
|
||||||
|
needs: [ backend-tests, frontend-tests ]
|
||||||
|
if: "!failure() && !cancelled()"
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-maven.yml@main
|
||||||
|
secrets:
|
||||||
|
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
|
||||||
|
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
|
||||||
|
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
||||||
|
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
||||||
|
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
||||||
|
|
||||||
|
publish-github:
|
||||||
|
name: Github Release
|
||||||
|
needs: [build-artifacts, backend-tests, frontend-tests]
|
||||||
|
if: "!failure() && !cancelled()"
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-github.yml@main
|
||||||
|
secrets:
|
||||||
|
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
|
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||||
@@ -3,11 +3,11 @@ name: Pull Request - Delete Docker
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
# TODO import a reusable one
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: Pull Request - Delete Docker
|
name: Pull Request - Delete Docker
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
|
if: github.repository == 'kestra-io/kestra' # prevent running on forks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
- uses: dataaxiom/ghcr-cleanup-action@v1
|
||||||
33
.github/workflows/pull-request.yml
vendored
33
.github/workflows/pull-request.yml
vendored
@@ -2,17 +2,12 @@ name: Pull Request Workflow
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}-pr
|
group: ${{ github.workflow }}-${{ github.ref_name }}-pr
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ********************************************************************************************************************
|
|
||||||
# File changes detection
|
|
||||||
# ********************************************************************************************************************
|
|
||||||
file-changes:
|
file-changes:
|
||||||
if: ${{ github.event.pull_request.draft == false }}
|
if: ${{ github.event.pull_request.draft == false }}
|
||||||
name: File changes detection
|
name: File changes detection
|
||||||
@@ -33,14 +28,11 @@ jobs:
|
|||||||
- '!{ui,.github}/**'
|
- '!{ui,.github}/**'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# ********************************************************************************************************************
|
|
||||||
# Tests
|
|
||||||
# ********************************************************************************************************************
|
|
||||||
frontend:
|
frontend:
|
||||||
name: Frontend - Tests
|
name: Frontend - Tests
|
||||||
needs: [file-changes]
|
needs: [file-changes]
|
||||||
if: "needs.file-changes.outputs.ui == 'true'"
|
if: "needs.file-changes.outputs.ui == 'true'"
|
||||||
uses: ./.github/workflows/workflow-frontend-test.yml
|
uses: kestra-io/actions/.github/workflows/kestra-oss-frontend-tests.yml@main
|
||||||
secrets:
|
secrets:
|
||||||
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -49,7 +41,7 @@ jobs:
|
|||||||
name: Backend - Tests
|
name: Backend - Tests
|
||||||
needs: file-changes
|
needs: file-changes
|
||||||
if: "needs.file-changes.outputs.backend == 'true'"
|
if: "needs.file-changes.outputs.backend == 'true'"
|
||||||
uses: ./.github/workflows/workflow-backend-test.yml
|
uses: kestra-io/actions/.github/workflows/kestra-oss-backend-tests.yml@main
|
||||||
secrets:
|
secrets:
|
||||||
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -58,21 +50,8 @@ jobs:
|
|||||||
|
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: E2E - Tests
|
name: E2E - Tests
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: kestra-io/actions/.github/workflows/kestra-oss-e2e-tests.yml@main
|
||||||
|
|
||||||
end:
|
generate-pull-request-docker-image:
|
||||||
name: End
|
name: Generate PR docker image
|
||||||
runs-on: ubuntu-latest
|
uses: kestra-io/actions/.github/workflows/kestra-oss-pullrequest-publish-docker.yml@main
|
||||||
if: always()
|
|
||||||
needs: [frontend, backend]
|
|
||||||
steps:
|
|
||||||
# Slack
|
|
||||||
- name: Slack notification
|
|
||||||
uses: Gamesight/slack-workflow-status@master
|
|
||||||
if: ${{ always() && env.SLACK_WEBHOOK_URL != 0 }}
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
||||||
name: GitHub Actions
|
|
||||||
icon_emoji: ":github-actions:"
|
|
||||||
channel: "C02DQ1A7JLR"
|
|
||||||
|
|||||||
34
.github/workflows/release-docker.yml
vendored
Normal file
34
.github/workflows/release-docker.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Publish docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
retag-latest:
|
||||||
|
description: 'Retag latest Docker images'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
retag-lts:
|
||||||
|
description: 'Retag LTS Docker images'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
dry-run:
|
||||||
|
description: 'Dry run mode that will not write or release anything'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-docker:
|
||||||
|
name: Publish Docker
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-docker.yml@main
|
||||||
|
with:
|
||||||
|
retag-latest: ${{ inputs.retag-latest }}
|
||||||
|
retag-lts: ${{ inputs.retag-lts }}
|
||||||
|
dry-run: ${{ inputs.dry-run }}
|
||||||
|
secrets:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||||
60
.github/workflows/setversion-tag-plugins.yml
vendored
60
.github/workflows/setversion-tag-plugins.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Set Version and Tag Plugins
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
releaseVersion:
|
|
||||||
description: 'The release version (e.g., 0.21.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
dryRun:
|
|
||||||
description: 'Use DRY_RUN mode'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
jobs:
|
|
||||||
tag:
|
|
||||||
name: Release plugins
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Checkout
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Get Plugins List
|
|
||||||
- name: Get Plugins List
|
|
||||||
uses: ./.github/actions/plugins-list
|
|
||||||
id: plugins-list
|
|
||||||
with:
|
|
||||||
plugin-version: 'LATEST'
|
|
||||||
|
|
||||||
- name: 'Configure Git'
|
|
||||||
run: |
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
- name: Set Version and Tag Plugins
|
|
||||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
|
||||||
env:
|
|
||||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
run: |
|
|
||||||
chmod +x ./dev-tools/setversion-tag-plugins.sh;
|
|
||||||
|
|
||||||
./dev-tools/setversion-tag-plugins.sh \
|
|
||||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
|
||||||
--yes \
|
|
||||||
${{ steps.plugins-list.outputs.repositories }}
|
|
||||||
|
|
||||||
- name: Set Version and Tag Plugins (DRY_RUN)
|
|
||||||
if: ${{ github.event.inputs.dryRun == 'true' }}
|
|
||||||
env:
|
|
||||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
run: |
|
|
||||||
chmod +x ./dev-tools/setversion-tag-plugins.sh;
|
|
||||||
|
|
||||||
./dev-tools/setversion-tag-plugins.sh \
|
|
||||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
|
||||||
--dry-run \
|
|
||||||
--yes \
|
|
||||||
${{ steps.plugins-list.outputs.repositories }}
|
|
||||||
60
.github/workflows/setversion-tag.yml
vendored
60
.github/workflows/setversion-tag.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Set Version and Tag
|
|
||||||
run-name: "Set version and Tag Kestra to ${{ github.event.inputs.releaseVersion }} 🚀"
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
releaseVersion:
|
|
||||||
description: 'The release version (e.g., 0.21.1)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
env:
|
|
||||||
RELEASE_VERSION: "${{ github.event.inputs.releaseVersion }}"
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release Kestra
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: startsWith(github.ref, 'refs/heads/releases/v')
|
|
||||||
steps:
|
|
||||||
# Checks
|
|
||||||
- name: Check Inputs
|
|
||||||
run: |
|
|
||||||
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+)(\.[0-9]+)(-rc[0-9])?(-SNAPSHOT)?$ ]]; then
|
|
||||||
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)(\.[0-9]+)-(rc[0-9])?(-SNAPSHOT)?$"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 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
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Checkout
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
|
|
||||||
# Configure
|
|
||||||
- name: Git - Configure
|
|
||||||
run: |
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
- name: Run Gradle Release
|
|
||||||
env:
|
|
||||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Update version
|
|
||||||
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
|
||||||
git add ./gradle.properties
|
|
||||||
git commit -m"chore(version): update to version '$RELEASE_VERSION'"
|
|
||||||
git push
|
|
||||||
git tag -a "v$RELEASE_VERSION" -m"v$RELEASE_VERSION"
|
|
||||||
git push --tags
|
|
||||||
99
.github/workflows/vulnerabilities-check.yml
vendored
99
.github/workflows/vulnerabilities-check.yml
vendored
@@ -21,20 +21,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# Checkout GitHub Actions
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
repository: kestra-io/actions
|
|
||||||
path: actions
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
# Setup build
|
# Setup build
|
||||||
- uses: ./actions/.github/actions/setup-build
|
- uses: kestra-io/actions/composite/setup-build@main
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
java-enabled: true
|
java-enabled: true
|
||||||
node-enabled: true
|
node-enabled: true
|
||||||
caches-enabled: true
|
|
||||||
|
|
||||||
# Npm
|
# Npm
|
||||||
- name: Npm - Install
|
- name: Npm - Install
|
||||||
@@ -56,92 +48,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: dependency-check-report
|
name: dependency-check-report
|
||||||
path: build/reports/dependency-check-report.html
|
path: build/reports/dependency-check-report.html
|
||||||
|
|
||||||
develop-image-check:
|
|
||||||
name: Image Check (develop)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
# Checkout
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Checkout GitHub Actions
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
repository: kestra-io/actions
|
|
||||||
path: actions
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: ./actions/.github/actions/setup-build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: false
|
|
||||||
node-enabled: false
|
|
||||||
caches-enabled: true
|
|
||||||
|
|
||||||
# Run Trivy image scan for Docker vulnerabilities, see https://github.com/aquasecurity/trivy-action
|
|
||||||
- name: Docker Vulnerabilities Check
|
|
||||||
uses: aquasecurity/trivy-action@0.33.0
|
|
||||||
with:
|
|
||||||
image-ref: kestra/kestra:develop
|
|
||||||
format: 'template'
|
|
||||||
template: '@/contrib/sarif.tpl'
|
|
||||||
severity: 'CRITICAL,HIGH'
|
|
||||||
output: 'trivy-results.sarif'
|
|
||||||
skip-dirs: /app/plugins
|
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
with:
|
|
||||||
sarif_file: 'trivy-results.sarif'
|
|
||||||
category: docker-
|
|
||||||
|
|
||||||
latest-image-check:
|
|
||||||
name: Image Check (latest)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
# Checkout
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Checkout GitHub Actions
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
repository: kestra-io/actions
|
|
||||||
path: actions
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: ./actions/.github/actions/setup-build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: false
|
|
||||||
node-enabled: false
|
|
||||||
caches-enabled: true
|
|
||||||
|
|
||||||
# Run Trivy image scan for Docker vulnerabilities, see https://github.com/aquasecurity/trivy-action
|
|
||||||
- name: Docker Vulnerabilities Check
|
|
||||||
uses: aquasecurity/trivy-action@0.33.0
|
|
||||||
with:
|
|
||||||
image-ref: kestra/kestra:latest
|
|
||||||
format: table
|
|
||||||
skip-dirs: /app/plugins
|
|
||||||
scanners: vuln
|
|
||||||
severity: 'CRITICAL,HIGH'
|
|
||||||
output: 'trivy-results.sarif'
|
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
with:
|
|
||||||
sarif_file: 'trivy-results.sarif'
|
|
||||||
142
.github/workflows/workflow-backend-test.yml
vendored
142
.github/workflows/workflow-backend-test.yml
vendored
@@ -1,142 +0,0 @@
|
|||||||
name: Backend - Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
secrets:
|
|
||||||
GITHUB_AUTH_TOKEN:
|
|
||||||
description: "The GitHub Token."
|
|
||||||
required: true
|
|
||||||
CODECOV_TOKEN:
|
|
||||||
description: 'Codecov Token'
|
|
||||||
required: true
|
|
||||||
SONAR_TOKEN:
|
|
||||||
description: 'Sonar Token'
|
|
||||||
required: true
|
|
||||||
GOOGLE_SERVICE_ACCOUNT:
|
|
||||||
description: 'Google Service Account'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
checks: write
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Backend - Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
name: Checkout - Current ref
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: kestra-io/actions/.github/actions/setup-build@main
|
|
||||||
name: Setup - Build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: true
|
|
||||||
node-enabled: true
|
|
||||||
python-enabled: true
|
|
||||||
|
|
||||||
# Services
|
|
||||||
- name: Setup - Start docker compose
|
|
||||||
shell: bash
|
|
||||||
run: docker compose -f docker-compose-ci.yml up -d
|
|
||||||
|
|
||||||
# Gradle check
|
|
||||||
- name: Gradle - Build
|
|
||||||
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
|
||||||
env:
|
|
||||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo $GOOGLE_SERVICE_ACCOUNT | base64 -d > ~/.gcp-service-account.json
|
|
||||||
export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.gcp-service-account.json
|
|
||||||
./gradlew check javadoc --parallel
|
|
||||||
|
|
||||||
# report test
|
|
||||||
- name: Test - Publish Test Results
|
|
||||||
uses: dorny/test-reporter@v2
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: Java Tests Report
|
|
||||||
reporter: java-junit
|
|
||||||
path: '**/build/test-results/test/TEST-*.xml'
|
|
||||||
list-suites: 'failed'
|
|
||||||
list-tests: 'failed'
|
|
||||||
fail-on-error: 'false'
|
|
||||||
token: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
|
||||||
|
|
||||||
# Sonar
|
|
||||||
- name: Test - Analyze with Sonar
|
|
||||||
if: env.SONAR_TOKEN != ''
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
shell: bash
|
|
||||||
run: ./gradlew sonar --info
|
|
||||||
|
|
||||||
# GCP
|
|
||||||
- name: GCP - Auth with unit test account
|
|
||||||
id: auth
|
|
||||||
if: always() && env.GOOGLE_SERVICE_ACCOUNT != ''
|
|
||||||
continue-on-error: true
|
|
||||||
uses: "google-github-actions/auth@v3"
|
|
||||||
with:
|
|
||||||
credentials_json: "${{ secrets.GOOGLE_SERVICE_ACCOUNT }}"
|
|
||||||
|
|
||||||
- name: GCP - Setup Cloud SDK
|
|
||||||
if: env.GOOGLE_SERVICE_ACCOUNT != ''
|
|
||||||
uses: "google-github-actions/setup-gcloud@v3"
|
|
||||||
|
|
||||||
# Allure check
|
|
||||||
- uses: rlespinasse/github-slug-action@v5
|
|
||||||
name: Allure - Generate slug variables
|
|
||||||
|
|
||||||
- name: Allure - Publish report
|
|
||||||
uses: andrcuns/allure-publish-action@v2.9.0
|
|
||||||
if: always() && env.GOOGLE_SERVICE_ACCOUNT != ''
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
|
||||||
JAVA_HOME: /usr/lib/jvm/default-jvm/
|
|
||||||
with:
|
|
||||||
storageType: gcs
|
|
||||||
resultsGlob: "**/build/allure-results"
|
|
||||||
bucket: internal-kestra-host
|
|
||||||
baseUrl: "https://internal.dev.kestra.io"
|
|
||||||
prefix: ${{ format('{0}/{1}', github.repository, 'allure/java') }}
|
|
||||||
copyLatest: true
|
|
||||||
ignoreMissingResults: true
|
|
||||||
|
|
||||||
# Jacoco
|
|
||||||
- name: Jacoco - Copy reports
|
|
||||||
if: env.GOOGLE_SERVICE_ACCOUNT != ''
|
|
||||||
continue-on-error: true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mv build/reports/jacoco/testCodeCoverageReport build/reports/jacoco/test/
|
|
||||||
mv build/reports/jacoco/test/testCodeCoverageReport.xml build/reports/jacoco/test/jacocoTestReport.xml
|
|
||||||
gsutil -m rsync -d -r build/reports/jacoco/test/ gs://internal-kestra-host/${{ format('{0}/{1}', github.repository, 'jacoco') }}
|
|
||||||
|
|
||||||
# Codecov
|
|
||||||
- name: Codecov - Upload coverage reports
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
flags: backend
|
|
||||||
|
|
||||||
- name: Codecov - Upload test results
|
|
||||||
uses: codecov/test-results-action@v1
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
flags: backend
|
|
||||||
80
.github/workflows/workflow-build-artifacts.yml
vendored
80
.github/workflows/workflow-build-artifacts.yml
vendored
@@ -1,80 +0,0 @@
|
|||||||
name: Build Artifacts
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build - Artifacts
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
docker-tag: ${{ steps.vars.outputs.tag }}
|
|
||||||
docker-artifact-name: ${{ steps.vars.outputs.artifact }}
|
|
||||||
plugins: ${{ steps.plugins.outputs.plugins }}
|
|
||||||
env:
|
|
||||||
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout - Current ref
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Npm
|
|
||||||
- name: Setup - Npm install
|
|
||||||
shell: bash
|
|
||||||
working-directory: ui
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- uses: kestra-io/actions/.github/actions/setup-build@main
|
|
||||||
name: Setup - Build
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: true
|
|
||||||
node-enabled: true
|
|
||||||
|
|
||||||
# Get Plugins List
|
|
||||||
- name: Plugins - Get List
|
|
||||||
uses: ./.github/actions/plugins-list
|
|
||||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
|
||||||
id: plugins-list
|
|
||||||
with:
|
|
||||||
plugin-version: ${{ env.PLUGIN_VERSION }}
|
|
||||||
|
|
||||||
# Set Plugins List
|
|
||||||
- name: Plugins - Set List
|
|
||||||
id: plugins
|
|
||||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
PLUGINS="${{ steps.plugins-list.outputs.plugins }}"
|
|
||||||
TAG=${GITHUB_REF#refs/*/}
|
|
||||||
if [[ $TAG = "master" || $TAG == v* ]]; then
|
|
||||||
echo "plugins=$PLUGINS" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "plugins=--repositories=https://central.sonatype.com/repository/maven-snapshots/ $PLUGINS" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build
|
|
||||||
- name: Gradle - Build
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
./gradlew executableJar
|
|
||||||
|
|
||||||
- name: Artifacts - Copy exe to image
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra
|
|
||||||
|
|
||||||
# Upload artifacts
|
|
||||||
- name: Artifacts - Upload JAR
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: jar
|
|
||||||
path: build/libs/
|
|
||||||
|
|
||||||
- name: Artifacts - Upload Executable
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: exe
|
|
||||||
path: build/executable/
|
|
||||||
70
.github/workflows/workflow-frontend-test.yml
vendored
70
.github/workflows/workflow-frontend-test.yml
vendored
@@ -1,70 +0,0 @@
|
|||||||
name: Frontend - Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
secrets:
|
|
||||||
GITHUB_AUTH_TOKEN:
|
|
||||||
description: "The GitHub Token."
|
|
||||||
required: true
|
|
||||||
CODECOV_TOKEN:
|
|
||||||
description: 'Codecov Token'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
# to save corepack from itself
|
|
||||||
COREPACK_INTEGRITY_KEYS: 0
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Frontend - Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Cache Node Modules
|
|
||||||
id: cache-node-modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
ui/node_modules
|
|
||||||
key: modules-${{ hashFiles('ui/package-lock.json') }}
|
|
||||||
|
|
||||||
- name: Cache Playwright Binaries
|
|
||||||
id: cache-playwright
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
key: playwright-${{ hashFiles('ui/package-lock.json') }}
|
|
||||||
|
|
||||||
- name: Npm - install
|
|
||||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
||||||
working-directory: ui
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Npm - lint
|
|
||||||
uses: reviewdog/action-eslint@v1
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
|
||||||
reporter: github-pr-review
|
|
||||||
workdir: ui
|
|
||||||
|
|
||||||
- name: Npm - Run build
|
|
||||||
working-directory: ui
|
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Run front-end unit tests
|
|
||||||
working-directory: ui
|
|
||||||
run: npm run test:unit -- --coverage
|
|
||||||
|
|
||||||
- name: Storybook - Install Playwright
|
|
||||||
working-directory: ui
|
|
||||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
|
||||||
run: npx playwright install --with-deps
|
|
||||||
|
|
||||||
- name: Run storybook component tests
|
|
||||||
working-directory: ui
|
|
||||||
run: npm run test:storybook -- --coverage
|
|
||||||
88
.github/workflows/workflow-github-release.yml
vendored
88
.github/workflows/workflow-github-release.yml
vendored
@@ -1,88 +0,0 @@
|
|||||||
name: Github - Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
workflow_call:
|
|
||||||
secrets:
|
|
||||||
GH_PERSONAL_TOKEN:
|
|
||||||
description: "The Github personal token."
|
|
||||||
required: true
|
|
||||||
SLACK_RELEASES_WEBHOOK_URL:
|
|
||||||
description: "The Slack webhook URL."
|
|
||||||
required: true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Github - Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Check out
|
|
||||||
- name: Checkout - Repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
# Checkout GitHub Actions
|
|
||||||
- name: Checkout - Actions
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
repository: kestra-io/actions
|
|
||||||
sparse-checkout-cone-mode: true
|
|
||||||
path: actions
|
|
||||||
sparse-checkout: |
|
|
||||||
.github/actions
|
|
||||||
|
|
||||||
# Download Exec
|
|
||||||
# Must be done after checkout actions
|
|
||||||
- name: Artifacts - Download executable
|
|
||||||
uses: actions/download-artifact@v5
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
with:
|
|
||||||
name: exe
|
|
||||||
path: build/executable
|
|
||||||
|
|
||||||
- name: Check if current tag is latest
|
|
||||||
id: is_latest
|
|
||||||
run: |
|
|
||||||
latest_tag=$(git tag | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -V | tail -n1)
|
|
||||||
current_tag="${GITHUB_REF_NAME#v}"
|
|
||||||
if [ "$current_tag" = "$latest_tag" ]; then
|
|
||||||
echo "latest=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "latest=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
# GitHub Release
|
|
||||||
- name: Create GitHub release
|
|
||||||
uses: ./actions/.github/actions/github-release
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
env:
|
|
||||||
MAKE_LATEST: ${{ steps.is_latest.outputs.latest }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_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
|
|
||||||
uses: peter-evans/repository-dispatch@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
repository: kestra-io/helm-charts
|
|
||||||
event-type: update-helm-chart-version
|
|
||||||
client-payload: |-
|
|
||||||
{
|
|
||||||
"new_version": "${{ github.ref_name }}",
|
|
||||||
"github_repository": "${{ github.repository }}",
|
|
||||||
"github_actor": "${{ github.actor }}"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Merge Release Notes
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
uses: ./actions/.github/actions/github-release-note-merge
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
RELEASE_TAG: ${{ github.ref_name }}
|
|
||||||
200
.github/workflows/workflow-publish-docker.yml
vendored
200
.github/workflows/workflow-publish-docker.yml
vendored
@@ -1,200 +0,0 @@
|
|||||||
name: Create Docker images on Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
retag-latest:
|
|
||||||
description: 'Retag latest Docker images'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
default: "false"
|
|
||||||
options:
|
|
||||||
- "true"
|
|
||||||
- "false"
|
|
||||||
release-tag:
|
|
||||||
description: 'Kestra Release Tag (by default, deduced with the ref)'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
plugin-version:
|
|
||||||
description: 'Plugin version'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: "LATEST"
|
|
||||||
force-download-artifact:
|
|
||||||
description: 'Force download artifact'
|
|
||||||
required: false
|
|
||||||
type: choice
|
|
||||||
default: "true"
|
|
||||||
options:
|
|
||||||
- "true"
|
|
||||||
- "false"
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
plugin-version:
|
|
||||||
description: "Plugin version"
|
|
||||||
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."
|
|
||||||
required: true
|
|
||||||
DOCKERHUB_PASSWORD:
|
|
||||||
description: "The Dockerhub password."
|
|
||||||
required: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
PLUGIN_VERSION: ${{ inputs.plugin-version != null && inputs.plugin-version || 'LATEST' }}
|
|
||||||
jobs:
|
|
||||||
plugins:
|
|
||||||
name: List Plugins
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
plugins: ${{ steps.plugins.outputs.plugins }}
|
|
||||||
steps:
|
|
||||||
# Checkout
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
# Get Plugins List
|
|
||||||
- name: Get Plugins List
|
|
||||||
uses: ./.github/actions/plugins-list
|
|
||||||
id: plugins
|
|
||||||
with: # remap LATEST-SNAPSHOT to LATEST
|
|
||||||
plugin-version: ${{ env.PLUGIN_VERSION == 'LATEST-SNAPSHOT' && 'LATEST' || env.PLUGIN_VERSION }}
|
|
||||||
|
|
||||||
# ********************************************************************************************************************
|
|
||||||
# Build
|
|
||||||
# ********************************************************************************************************************
|
|
||||||
build-artifacts:
|
|
||||||
name: Build Artifacts
|
|
||||||
if: ${{ inputs.force-download-artifact == 'true' }}
|
|
||||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
|
||||||
|
|
||||||
docker:
|
|
||||||
name: Publish Docker
|
|
||||||
needs: [ plugins, build-artifacts ]
|
|
||||||
if: always()
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
image:
|
|
||||||
- name: "-no-plugins"
|
|
||||||
plugins: ""
|
|
||||||
packages: jattach
|
|
||||||
python-libs: ""
|
|
||||||
- name: ""
|
|
||||||
plugins: ${{needs.plugins.outputs.plugins}}
|
|
||||||
packages: python3 python-is-python3 python3-pip curl jattach
|
|
||||||
python-libs: kestra
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
# Vars
|
|
||||||
- name: Set image name
|
|
||||||
id: vars
|
|
||||||
run: |
|
|
||||||
if [[ "${{ inputs.release-tag }}" == "" ]]; then
|
|
||||||
TAG=${GITHUB_REF#refs/*/}
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
TAG="${{ inputs.release-tag }}"
|
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
|
||||||
if [[ $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
# this will remove the patch version number
|
|
||||||
MINOR_SEMVER=${TAG%.*}
|
|
||||||
echo "minor_semver=${MINOR_SEMVER}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "Tag '$TAG' is not a valid semver (vMAJOR.MINOR.PATCH), skipping minor_semver"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then
|
|
||||||
echo "plugins=--repositories=https://central.sonatype.com/repository/maven-snapshots/ ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT;
|
|
||||||
else
|
|
||||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Download executable from artifact
|
|
||||||
- name: Artifacts - Download executable
|
|
||||||
uses: actions/download-artifact@v5
|
|
||||||
with:
|
|
||||||
name: exe
|
|
||||||
path: build/executable
|
|
||||||
|
|
||||||
- name: Copy exe to image
|
|
||||||
run: |
|
|
||||||
cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra
|
|
||||||
|
|
||||||
# Docker setup
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Docker - Fix Qemu
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
# Docker Login
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
# Docker Build and push
|
|
||||||
- name: Push to Docker Hub
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
build-args: |
|
|
||||||
KESTRA_PLUGINS=${{ steps.vars.outputs.plugins }}
|
|
||||||
APT_PACKAGES=${{ matrix.image.packages }}
|
|
||||||
PYTHON_LIBRARIES=${{ matrix.image.python-libs }}
|
|
||||||
|
|
||||||
- name: Install regctl
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
uses: regclient/actions/regctl-installer@main
|
|
||||||
|
|
||||||
- name: Retag to minor semver version
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v') && steps.vars.outputs.minor_semver != ''
|
|
||||||
run: |
|
|
||||||
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.minor_semver, matrix.image.name) }}
|
|
||||||
|
|
||||||
- name: Retag to latest
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v') && inputs.retag-latest == 'true'
|
|
||||||
run: |
|
|
||||||
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest{0}', matrix.image.name) }}
|
|
||||||
|
|
||||||
end:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- docker
|
|
||||||
if: always()
|
|
||||||
env:
|
|
||||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
||||||
steps:
|
|
||||||
|
|
||||||
# Slack
|
|
||||||
- name: Slack notification
|
|
||||||
uses: Gamesight/slack-workflow-status@master
|
|
||||||
if: ${{ always() && env.SLACK_WEBHOOK_URL != 0 }}
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
||||||
name: GitHub Actions
|
|
||||||
icon_emoji: ':github-actions:'
|
|
||||||
channel: 'C02DQ1A7JLR' # _int_git channel
|
|
||||||
57
.github/workflows/workflow-publish-maven.yml
vendored
57
.github/workflows/workflow-publish-maven.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: Publish - Maven
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
secrets:
|
|
||||||
SONATYPE_USER:
|
|
||||||
description: "The Sonatype username."
|
|
||||||
required: true
|
|
||||||
SONATYPE_PASSWORD:
|
|
||||||
description: "The Sonatype password."
|
|
||||||
required: true
|
|
||||||
SONATYPE_GPG_KEYID:
|
|
||||||
description: "The Sonatype GPG key id."
|
|
||||||
required: true
|
|
||||||
SONATYPE_GPG_PASSWORD:
|
|
||||||
description: "The Sonatype GPG password."
|
|
||||||
required: true
|
|
||||||
SONATYPE_GPG_FILE:
|
|
||||||
description: "The Sonatype GPG file."
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Publish - Maven
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout - Current ref
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
# Setup build
|
|
||||||
- name: Setup - Build
|
|
||||||
uses: kestra-io/actions/.github/actions/setup-build@main
|
|
||||||
id: build
|
|
||||||
with:
|
|
||||||
java-enabled: true
|
|
||||||
node-enabled: true
|
|
||||||
|
|
||||||
# Publish
|
|
||||||
- name: Publish - Release package to Maven Central
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USER }}
|
|
||||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
|
||||||
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
|
||||||
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
|
||||||
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE}}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle/
|
|
||||||
echo "signing.keyId=${SONATYPE_GPG_KEYID}" > ~/.gradle/gradle.properties
|
|
||||||
echo "signing.password=${SONATYPE_GPG_PASSWORD}" >> ~/.gradle/gradle.properties
|
|
||||||
echo "signing.secretKeyRingFile=${HOME}/.gradle/secring.gpg" >> ~/.gradle/gradle.properties
|
|
||||||
echo ${SONATYPE_GPG_FILE} | base64 -d > ~/.gradle/secring.gpg
|
|
||||||
./gradlew publishToMavenCentral
|
|
||||||
|
|
||||||
# Gradle dependency
|
|
||||||
- name: Java - Gradle dependency graph
|
|
||||||
uses: gradle/actions/dependency-submission@v4
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
name: Pull Request - Publish Docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-artifacts:
|
|
||||||
name: Build Artifacts
|
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
|
|
||||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
|
||||||
|
|
||||||
publish:
|
|
||||||
name: Publish Docker
|
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-artifacts
|
|
||||||
env:
|
|
||||||
GITHUB_IMAGE_PATH: "ghcr.io/kestra-io/kestra-pr"
|
|
||||||
steps:
|
|
||||||
- name: Checkout - Current ref
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Docker setup
|
|
||||||
- name: Docker - Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Docker - Setup Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
# Docker Login
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
# Build Docker Image
|
|
||||||
- name: Artifacts - Download executable
|
|
||||||
uses: actions/download-artifact@v5
|
|
||||||
with:
|
|
||||||
name: exe
|
|
||||||
path: build/executable
|
|
||||||
|
|
||||||
- name: Docker - Copy exe to image
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra
|
|
||||||
|
|
||||||
- name: Docker - Build image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.pr
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.GITHUB_IMAGE_PATH }}:${{ github.event.pull_request.number }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
|
|
||||||
# Add comment on pull request
|
|
||||||
- name: Add comment to PR
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
body: `**🐋 Docker image**: \`${{ env.GITHUB_IMAGE_PATH }}:${{ github.event.pull_request.number }}\`\n` +
|
|
||||||
`\n` +
|
|
||||||
`\`\`\`bash\n` +
|
|
||||||
`docker run --pull=always --rm -it -p 8080:8080 --user=root -v /var/run/docker.sock:/var/run/docker.sock -v /tmp:/tmp ${{ env.GITHUB_IMAGE_PATH }}:${{ github.event.pull_request.number }} server local\n` +
|
|
||||||
`\`\`\``
|
|
||||||
})
|
|
||||||
85
.github/workflows/workflow-release.yml
vendored
85
.github/workflows/workflow-release.yml
vendored
@@ -1,85 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
plugin-version:
|
|
||||||
description: "plugins version"
|
|
||||||
default: 'LATEST'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
publish-docker:
|
|
||||||
description: "Publish Docker image"
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
plugin-version:
|
|
||||||
description: "plugins version"
|
|
||||||
default: 'LATEST'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
secrets:
|
|
||||||
DOCKERHUB_USERNAME:
|
|
||||||
description: "The Dockerhub username."
|
|
||||||
required: true
|
|
||||||
DOCKERHUB_PASSWORD:
|
|
||||||
description: "The Dockerhub password."
|
|
||||||
required: true
|
|
||||||
SONATYPE_USER:
|
|
||||||
description: "The Sonatype username."
|
|
||||||
required: true
|
|
||||||
SONATYPE_PASSWORD:
|
|
||||||
description: "The Sonatype password."
|
|
||||||
required: true
|
|
||||||
SONATYPE_GPG_KEYID:
|
|
||||||
description: "The Sonatype GPG key id."
|
|
||||||
required: true
|
|
||||||
SONATYPE_GPG_PASSWORD:
|
|
||||||
description: "The Sonatype GPG password."
|
|
||||||
required: true
|
|
||||||
SONATYPE_GPG_FILE:
|
|
||||||
description: "The Sonatype GPG file."
|
|
||||||
required: true
|
|
||||||
GH_PERSONAL_TOKEN:
|
|
||||||
description: "GH personnal Token."
|
|
||||||
required: true
|
|
||||||
SLACK_RELEASES_WEBHOOK_URL:
|
|
||||||
description: "Slack webhook for releases channel."
|
|
||||||
required: true
|
|
||||||
jobs:
|
|
||||||
build-artifacts:
|
|
||||||
name: Build - Artifacts
|
|
||||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
|
||||||
|
|
||||||
Docker:
|
|
||||||
name: Publish Docker
|
|
||||||
needs: build-artifacts
|
|
||||||
uses: ./.github/workflows/workflow-publish-docker.yml
|
|
||||||
if: github.ref == 'refs/heads/develop' || inputs.publish-docker == 'true'
|
|
||||||
with:
|
|
||||||
force-download-artifact: 'false'
|
|
||||||
plugin-version: ${{ inputs.plugin-version != null && inputs.plugin-version || 'LATEST' }}
|
|
||||||
secrets:
|
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
Maven:
|
|
||||||
name: Publish Maven
|
|
||||||
uses: ./.github/workflows/workflow-publish-maven.yml
|
|
||||||
secrets:
|
|
||||||
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
|
|
||||||
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
|
|
||||||
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
|
||||||
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
|
||||||
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
|
||||||
|
|
||||||
Github:
|
|
||||||
name: Github Release
|
|
||||||
needs: build-artifacts
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
uses: ./.github/workflows/workflow-github-release.yml
|
|
||||||
secrets:
|
|
||||||
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
|
||||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
|
||||||
97
.github/workflows/workflow-test.yml
vendored
97
.github/workflows/workflow-test.yml
vendored
@@ -1,97 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 4 * * 1,2,3,4,5'
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
report-status:
|
|
||||||
description: "Report status of the jobs in outputs"
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
outputs:
|
|
||||||
frontend_status:
|
|
||||||
description: "Status of the frontend job"
|
|
||||||
value: ${{ jobs.set-frontend-status.outputs.frontend_status }}
|
|
||||||
backend_status:
|
|
||||||
description: "Status of the backend job"
|
|
||||||
value: ${{ jobs.set-backend-status.outputs.backend_status }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
file-changes:
|
|
||||||
name: File changes detection
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
outputs:
|
|
||||||
ui: ${{ steps.changes.outputs.ui }}
|
|
||||||
backend: ${{ steps.changes.outputs.backend }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
|
||||||
id: changes
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
ui:
|
|
||||||
- 'ui/**'
|
|
||||||
backend:
|
|
||||||
- '!{ui,.github}/**'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
name: Frontend - Tests
|
|
||||||
needs: file-changes
|
|
||||||
if: "needs.file-changes.outputs.ui == 'true' || startsWith(github.ref, 'refs/tags/v')"
|
|
||||||
uses: ./.github/workflows/workflow-frontend-test.yml
|
|
||||||
secrets:
|
|
||||||
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
|
|
||||||
backend:
|
|
||||||
name: Backend - Tests
|
|
||||||
needs: file-changes
|
|
||||||
if: "needs.file-changes.outputs.backend == 'true' || startsWith(github.ref, 'refs/tags/v')"
|
|
||||||
uses: ./.github/workflows/workflow-backend-test.yml
|
|
||||||
secrets:
|
|
||||||
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
|
||||||
|
|
||||||
# Output every job status
|
|
||||||
# To be used in other workflows
|
|
||||||
report-status:
|
|
||||||
name: Report Status
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ frontend, backend ]
|
|
||||||
if: always() && (inputs.report-status == 'true')
|
|
||||||
outputs:
|
|
||||||
frontend_status: ${{ steps.set-frontend-status.outputs.frontend_status }}
|
|
||||||
backend_status: ${{ steps.set-backend-status.outputs.backend_status }}
|
|
||||||
steps:
|
|
||||||
- id: set-frontend-status
|
|
||||||
name: Set frontend job status
|
|
||||||
run: echo "::set-output name=frontend_status::${{ needs.frontend.result }}"
|
|
||||||
|
|
||||||
- id: set-backend-status
|
|
||||||
name: Set backend job status
|
|
||||||
run: echo "::set-output name=backend_status::${{ needs.backend.result }}"
|
|
||||||
|
|
||||||
notify:
|
|
||||||
name: Notify - Slack
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ frontend, backend ]
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
steps:
|
|
||||||
- name: Notify failed CI
|
|
||||||
id: send-ci-failed
|
|
||||||
if: |
|
|
||||||
always() && (needs.frontend.result != 'success' ||
|
|
||||||
needs.backend.result != 'success')
|
|
||||||
uses: kestra-io/actions/.github/actions/send-ci-failed@main
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM kestra/kestra:develop
|
ARG KESTRA_DOCKER_BASE_VERSION=develop
|
||||||
|
FROM kestra/kestra:$KESTRA_DOCKER_BASE_VERSION
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
62
build.gradle
62
build.gradle
@@ -205,23 +205,59 @@ subprojects {
|
|||||||
testImplementation 'org.assertj:assertj-core'
|
testImplementation 'org.assertj:assertj-core'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
def commonTestConfig = { Test t ->
|
||||||
useJUnitPlatform()
|
|
||||||
|
|
||||||
// set Xmx for test workers
|
// set Xmx for test workers
|
||||||
maxHeapSize = '4g'
|
t.maxHeapSize = '4g'
|
||||||
|
|
||||||
// configure en_US default locale for tests
|
// configure en_US default locale for tests
|
||||||
systemProperty 'user.language', 'en'
|
t.systemProperty 'user.language', 'en'
|
||||||
systemProperty 'user.country', 'US'
|
t.systemProperty 'user.country', 'US'
|
||||||
|
|
||||||
environment 'SECRET_MY_SECRET', "{\"secretKey\":\"secretValue\"}".bytes.encodeBase64().toString()
|
t.environment 'SECRET_MY_SECRET', "{\"secretKey\":\"secretValue\"}".bytes.encodeBase64().toString()
|
||||||
environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n"
|
t.environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n"
|
||||||
environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString()
|
t.environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString()
|
||||||
environment 'SECRET_NON_B64_SECRET', "some secret value"
|
t.environment 'SECRET_NON_B64_SECRET', "some secret value"
|
||||||
environment 'SECRET_PASSWORD', "cGFzc3dvcmQ="
|
t.environment 'SECRET_PASSWORD', "cGFzc3dvcmQ="
|
||||||
environment 'ENV_TEST1', "true"
|
t.environment 'ENV_TEST1', "true"
|
||||||
environment 'ENV_TEST2', "Pass by env"
|
t.environment 'ENV_TEST2', "Pass by env"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('flakyTest', Test) { Test t ->
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Runs tests tagged @Flaky but does not fail the build.'
|
||||||
|
|
||||||
|
useJUnitPlatform {
|
||||||
|
includeTags 'flaky'
|
||||||
|
}
|
||||||
|
ignoreFailures = true
|
||||||
|
|
||||||
|
reports {
|
||||||
|
junitXml.required = true
|
||||||
|
junitXml.outputPerTestCase = true
|
||||||
|
junitXml.mergeReruns = true
|
||||||
|
junitXml.includeSystemErrLog = true
|
||||||
|
junitXml.outputLocation = layout.buildDirectory.dir("test-results/flakyTest")
|
||||||
|
}
|
||||||
|
commonTestConfig(t)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform {
|
||||||
|
excludeTags 'flaky'
|
||||||
|
}
|
||||||
|
reports {
|
||||||
|
junitXml.required = true
|
||||||
|
junitXml.outputPerTestCase = true
|
||||||
|
junitXml.mergeReruns = true
|
||||||
|
junitXml.includeSystemErrLog = true
|
||||||
|
junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
|
||||||
|
}
|
||||||
|
commonTestConfig(it)
|
||||||
|
|
||||||
|
|
||||||
|
finalizedBy(tasks.named('flakyTest'))
|
||||||
}
|
}
|
||||||
|
|
||||||
testlogger {
|
testlogger {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public abstract class AbstractValidateCommand extends AbstractApiCommand {
|
|||||||
|
|
||||||
try(DefaultHttpClient client = client()) {
|
try(DefaultHttpClient client = client()) {
|
||||||
MutableHttpRequest<String> request = HttpRequest
|
MutableHttpRequest<String> request = HttpRequest
|
||||||
.POST(apiUri("/flows/validate", tenantService.getTenantId(tenantId)), body).contentType(MediaType.APPLICATION_YAML);
|
.POST(apiUri("/flows/validate", tenantService.getTenantIdAndAllowEETenants(tenantId)), body).contentType(MediaType.APPLICATION_YAML);
|
||||||
|
|
||||||
List<ValidateConstraintViolation> validations = client.toBlocking().retrieve(
|
List<ValidateConstraintViolation> validations = client.toBlocking().retrieve(
|
||||||
this.requestOptions(request),
|
this.requestOptions(request),
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ public class FlowValidateCommand extends AbstractValidateCommand {
|
|||||||
private FlowService flowService;
|
private FlowService flowService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private TenantIdSelectorService tenantService;
|
private TenantIdSelectorService tenantIdSelectorService;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
@@ -39,7 +40,7 @@ public class FlowValidateCommand extends AbstractValidateCommand {
|
|||||||
FlowWithSource flow = (FlowWithSource) object;
|
FlowWithSource flow = (FlowWithSource) object;
|
||||||
List<String> warnings = new ArrayList<>();
|
List<String> warnings = new ArrayList<>();
|
||||||
warnings.addAll(flowService.deprecationPaths(flow).stream().map(deprecation -> deprecation + " is deprecated").toList());
|
warnings.addAll(flowService.deprecationPaths(flow).stream().map(deprecation -> deprecation + " is deprecated").toList());
|
||||||
warnings.addAll(flowService.warnings(flow, tenantService.getTenantId(tenantId)));
|
warnings.addAll(flowService.warnings(flow, tenantIdSelectorService.getTenantIdAndAllowEETenants(tenantId)));
|
||||||
return warnings;
|
return warnings;
|
||||||
},
|
},
|
||||||
(Object object) -> {
|
(Object object) -> {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class FlowNamespaceUpdateCommand extends AbstractServiceNamespaceUpdateCo
|
|||||||
}
|
}
|
||||||
try(DefaultHttpClient client = client()) {
|
try(DefaultHttpClient client = client()) {
|
||||||
MutableHttpRequest<String> request = HttpRequest
|
MutableHttpRequest<String> request = HttpRequest
|
||||||
.POST(apiUri("/flows/", tenantService.getTenantId(tenantId)) + namespace + "?delete=" + delete, body).contentType(MediaType.APPLICATION_YAML);
|
.POST(apiUri("/flows/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "?delete=" + delete, body).contentType(MediaType.APPLICATION_YAML);
|
||||||
|
|
||||||
List<UpdateResult> updated = client.toBlocking().retrieve(
|
List<UpdateResult> updated = client.toBlocking().retrieve(
|
||||||
this.requestOptions(request),
|
this.requestOptions(request),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class NamespaceFilesUpdateCommand extends AbstractApiCommand {
|
|||||||
|
|
||||||
try (var files = Files.walk(from); DefaultHttpClient client = client()) {
|
try (var files = Files.walk(from); DefaultHttpClient client = client()) {
|
||||||
if (delete) {
|
if (delete) {
|
||||||
client.toBlocking().exchange(this.requestOptions(HttpRequest.DELETE(apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/files?path=" + to, null)));
|
client.toBlocking().exchange(this.requestOptions(HttpRequest.DELETE(apiUri("/namespaces/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "/files?path=" + to, null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
KestraIgnore kestraIgnore = new KestraIgnore(from);
|
KestraIgnore kestraIgnore = new KestraIgnore(from);
|
||||||
@@ -67,7 +67,7 @@ public class NamespaceFilesUpdateCommand extends AbstractApiCommand {
|
|||||||
client.toBlocking().exchange(
|
client.toBlocking().exchange(
|
||||||
this.requestOptions(
|
this.requestOptions(
|
||||||
HttpRequest.POST(
|
HttpRequest.POST(
|
||||||
apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/files?path=" + destination,
|
apiUri("/namespaces/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "/files?path=" + destination,
|
||||||
body
|
body
|
||||||
).contentType(MediaType.MULTIPART_FORM_DATA)
|
).contentType(MediaType.MULTIPART_FORM_DATA)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class KvUpdateCommand extends AbstractApiCommand {
|
|||||||
Duration ttl = expiration == null ? null : Duration.parse(expiration);
|
Duration ttl = expiration == null ? null : Duration.parse(expiration);
|
||||||
MutableHttpRequest<String> request = HttpRequest
|
MutableHttpRequest<String> request = HttpRequest
|
||||||
.PUT(apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/kv/" + key, value)
|
.PUT(apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/kv/" + key, value)
|
||||||
.contentType(MediaType.APPLICATION_JSON_TYPE);
|
.contentType(MediaType.TEXT_PLAIN);
|
||||||
|
|
||||||
if (ttl != null) {
|
if (ttl != null) {
|
||||||
request.header("ttl", ttl.toString());
|
request.header("ttl", ttl.toString());
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
package io.kestra.cli.commands.servers;
|
package io.kestra.cli.commands.servers;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import io.kestra.cli.services.TenantIdSelectorService;
|
||||||
import io.kestra.core.models.ServerType;
|
import io.kestra.core.models.ServerType;
|
||||||
|
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||||
import io.kestra.core.runners.ExecutorInterface;
|
import io.kestra.core.runners.ExecutorInterface;
|
||||||
import io.kestra.executor.SkipExecutionService;
|
import io.kestra.core.services.SkipExecutionService;
|
||||||
import io.kestra.core.services.StartExecutorService;
|
import io.kestra.core.services.StartExecutorService;
|
||||||
import io.kestra.core.utils.Await;
|
import io.kestra.core.utils.Await;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -19,6 +23,9 @@ import java.util.Map;
|
|||||||
description = "Start the Kestra executor"
|
description = "Start the Kestra executor"
|
||||||
)
|
)
|
||||||
public class ExecutorCommand extends AbstractServerCommand {
|
public class ExecutorCommand extends AbstractServerCommand {
|
||||||
|
@CommandLine.Spec
|
||||||
|
CommandLine.Model.CommandSpec spec;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private ApplicationContext applicationContext;
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
@@ -28,22 +35,28 @@ public class ExecutorCommand extends AbstractServerCommand {
|
|||||||
@Inject
|
@Inject
|
||||||
private StartExecutorService startExecutorService;
|
private StartExecutorService startExecutorService;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "The list of execution identifiers to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"-f", "--flow-path"}, description = "Tenant identifier required to load flows from the specified path")
|
||||||
|
private File flowPath;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--tenant", description = "Tenant identifier, Required to load flows from path")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "List of execution IDs to skip, separated by commas; for troubleshooting only")
|
||||||
private List<String> skipExecutions = Collections.emptyList();
|
private List<String> skipExecutions = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "The list of flow identifiers (tenant|namespace|flowId) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "List of flow identifiers (tenant|namespace|flowId) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipFlows = Collections.emptyList();
|
private List<String> skipFlows = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "The list of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "List of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipNamespaces = Collections.emptyList();
|
private List<String> skipNamespaces = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "The list of tenants to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "List of tenants to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipTenants = Collections.emptyList();
|
private List<String> skipTenants = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--start-executors"}, split=",", description = "The list of Kafka Stream executors to start, separated by a command. Use it only with the Kafka queue, for debugging purpose.")
|
@CommandLine.Option(names = {"--start-executors"}, split=",", description = "List of Kafka Stream executors to start, separated by a command. Use it only with the Kafka queue; for debugging only")
|
||||||
private List<String> startExecutors = Collections.emptyList();
|
private List<String> startExecutors = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--not-start-executors"}, split=",", description = "The list of Kafka Stream executors to not start, separated by a command. Use it only with the Kafka queue, for debugging purpose.")
|
@CommandLine.Option(names = {"--not-start-executors"}, split=",", description = "Lst of Kafka Stream executors to not start, separated by a command. Use it only with the Kafka queue; for debugging only")
|
||||||
private List<String> notStartExecutors = Collections.emptyList();
|
private List<String> notStartExecutors = Collections.emptyList();
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -64,6 +77,16 @@ public class ExecutorCommand extends AbstractServerCommand {
|
|||||||
|
|
||||||
super.call();
|
super.call();
|
||||||
|
|
||||||
|
if (flowPath != null) {
|
||||||
|
try {
|
||||||
|
LocalFlowRepositoryLoader localFlowRepositoryLoader = applicationContext.getBean(LocalFlowRepositoryLoader.class);
|
||||||
|
TenantIdSelectorService tenantIdSelectorService = applicationContext.getBean(TenantIdSelectorService.class);
|
||||||
|
localFlowRepositoryLoader.load(tenantIdSelectorService.getTenantId(this.tenantId), this.flowPath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CommandLine.ParameterException(this.spec.commandLine(), "Invalid flow path", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExecutorInterface executorService = applicationContext.getBean(ExecutorInterface.class);
|
ExecutorInterface executorService = applicationContext.getBean(ExecutorInterface.class);
|
||||||
executorService.run();
|
executorService.run();
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import com.google.common.collect.ImmutableMap;
|
|||||||
import io.kestra.core.models.ServerType;
|
import io.kestra.core.models.ServerType;
|
||||||
import io.kestra.core.runners.Indexer;
|
import io.kestra.core.runners.Indexer;
|
||||||
import io.kestra.core.utils.Await;
|
import io.kestra.core.utils.Await;
|
||||||
|
import io.kestra.core.services.SkipExecutionService;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
@@ -17,6 +20,11 @@ import java.util.Map;
|
|||||||
public class IndexerCommand extends AbstractServerCommand {
|
public class IndexerCommand extends AbstractServerCommand {
|
||||||
@Inject
|
@Inject
|
||||||
private ApplicationContext applicationContext;
|
private ApplicationContext applicationContext;
|
||||||
|
@Inject
|
||||||
|
private SkipExecutionService skipExecutionService;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting only")
|
||||||
|
private List<String> skipIndexerRecords = Collections.emptyList();
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static Map<String, Object> propertiesOverrides() {
|
public static Map<String, Object> propertiesOverrides() {
|
||||||
@@ -27,6 +35,8 @@ public class IndexerCommand extends AbstractServerCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
|
this.skipExecutionService.setSkipIndexerRecords(skipIndexerRecords);
|
||||||
|
|
||||||
super.call();
|
super.call();
|
||||||
|
|
||||||
Indexer indexer = applicationContext.getBean(Indexer.class);
|
Indexer indexer = applicationContext.getBean(Indexer.class);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import io.kestra.core.contexts.KestraContext;
|
|||||||
import io.kestra.core.models.ServerType;
|
import io.kestra.core.models.ServerType;
|
||||||
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||||
import io.kestra.cli.StandAloneRunner;
|
import io.kestra.cli.StandAloneRunner;
|
||||||
import io.kestra.executor.SkipExecutionService;
|
import io.kestra.core.services.SkipExecutionService;
|
||||||
import io.kestra.core.services.StartExecutorService;
|
import io.kestra.core.services.StartExecutorService;
|
||||||
import io.kestra.core.utils.Await;
|
import io.kestra.core.utils.Await;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
@@ -42,7 +42,7 @@ public class StandAloneCommand extends AbstractServerCommand {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private FileChangedEventListener fileWatcher;
|
private FileChangedEventListener fileWatcher;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"-f", "--flow-path"}, description = "the flow path containing flow to inject at startup (when running with a memory flow repository)")
|
@CommandLine.Option(names = {"-f", "--flow-path"}, description = "Tenant identifier required to load flows from the specified path")
|
||||||
private File flowPath;
|
private File flowPath;
|
||||||
|
|
||||||
@CommandLine.Option(names = "--tenant", description = "Tenant identifier, Required to load flows from path with the enterprise edition")
|
@CommandLine.Option(names = "--tenant", description = "Tenant identifier, Required to load flows from path with the enterprise edition")
|
||||||
@@ -51,18 +51,21 @@ public class StandAloneCommand extends AbstractServerCommand {
|
|||||||
@CommandLine.Option(names = {"--worker-thread"}, description = "the number of worker threads, defaults to eight times the number of available processors. Set it to 0 to avoid starting a worker.")
|
@CommandLine.Option(names = {"--worker-thread"}, description = "the number of worker threads, defaults to eight times the number of available processors. Set it to 0 to avoid starting a worker.")
|
||||||
private int workerThread = defaultWorkerThread();
|
private int workerThread = defaultWorkerThread();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "a list of execution identifiers to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "a list of execution identifiers to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipExecutions = Collections.emptyList();
|
private List<String> skipExecutions = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "a list of flow identifiers (namespace.flowId) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "a list of flow identifiers (namespace.flowId) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipFlows = Collections.emptyList();
|
private List<String> skipFlows = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "a list of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "a list of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipNamespaces = Collections.emptyList();
|
private List<String> skipNamespaces = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "a list of tenants to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "a list of tenants to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipTenants = Collections.emptyList();
|
private List<String> skipTenants = Collections.emptyList();
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting only")
|
||||||
|
private List<String> skipIndexerRecords = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--no-tutorials"}, description = "Flag to disable auto-loading of tutorial flows.")
|
@CommandLine.Option(names = {"--no-tutorials"}, description = "Flag to disable auto-loading of tutorial flows.")
|
||||||
boolean tutorialsDisabled = false;
|
boolean tutorialsDisabled = false;
|
||||||
|
|
||||||
@@ -93,6 +96,7 @@ public class StandAloneCommand extends AbstractServerCommand {
|
|||||||
this.skipExecutionService.setSkipFlows(skipFlows);
|
this.skipExecutionService.setSkipFlows(skipFlows);
|
||||||
this.skipExecutionService.setSkipNamespaces(skipNamespaces);
|
this.skipExecutionService.setSkipNamespaces(skipNamespaces);
|
||||||
this.skipExecutionService.setSkipTenants(skipTenants);
|
this.skipExecutionService.setSkipTenants(skipTenants);
|
||||||
|
this.skipExecutionService.setSkipIndexerRecords(skipIndexerRecords);
|
||||||
this.startExecutorService.applyOptions(startExecutors, notStartExecutors);
|
this.startExecutorService.applyOptions(startExecutors, notStartExecutors);
|
||||||
|
|
||||||
KestraContext.getContext().injectWorkerConfigs(workerThread, null);
|
KestraContext.getContext().injectWorkerConfigs(workerThread, null);
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import io.kestra.core.models.ServerType;
|
|||||||
import io.kestra.core.runners.Indexer;
|
import io.kestra.core.runners.Indexer;
|
||||||
import io.kestra.core.utils.Await;
|
import io.kestra.core.utils.Await;
|
||||||
import io.kestra.core.utils.ExecutorsUtils;
|
import io.kestra.core.utils.ExecutorsUtils;
|
||||||
|
import io.kestra.core.services.SkipExecutionService;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
@@ -28,11 +31,17 @@ public class WebServerCommand extends AbstractServerCommand {
|
|||||||
@Inject
|
@Inject
|
||||||
private ExecutorsUtils executorsUtils;
|
private ExecutorsUtils executorsUtils;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SkipExecutionService skipExecutionService;
|
||||||
|
|
||||||
@Option(names = {"--no-tutorials"}, description = "Flag to disable auto-loading of tutorial flows.")
|
@Option(names = {"--no-tutorials"}, description = "Flag to disable auto-loading of tutorial flows.")
|
||||||
boolean tutorialsDisabled = false;
|
private boolean tutorialsDisabled = false;
|
||||||
|
|
||||||
@Option(names = {"--no-indexer"}, description = "Flag to disable starting an embedded indexer.")
|
@Option(names = {"--no-indexer"}, description = "Flag to disable starting an embedded indexer.")
|
||||||
boolean indexerDisabled = false;
|
private boolean indexerDisabled = false;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting only")
|
||||||
|
private List<String> skipIndexerRecords = Collections.emptyList();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFlowAutoLoadEnabled() {
|
public boolean isFlowAutoLoadEnabled() {
|
||||||
@@ -48,6 +57,8 @@ public class WebServerCommand extends AbstractServerCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
|
this.skipExecutionService.setSkipIndexerRecords(skipIndexerRecords);
|
||||||
|
|
||||||
super.call();
|
super.call();
|
||||||
|
|
||||||
// start the indexer
|
// start the indexer
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.State;
|
|||||||
import io.kestra.core.queues.QueueFactoryInterface;
|
import io.kestra.core.queues.QueueFactoryInterface;
|
||||||
import io.kestra.core.queues.QueueInterface;
|
import io.kestra.core.queues.QueueInterface;
|
||||||
import io.kestra.core.runners.ExecutionQueued;
|
import io.kestra.core.runners.ExecutionQueued;
|
||||||
|
import io.kestra.core.services.ConcurrencyLimitService;
|
||||||
import io.kestra.jdbc.runner.AbstractJdbcExecutionQueuedStorage;
|
import io.kestra.jdbc.runner.AbstractJdbcExecutionQueuedStorage;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
@@ -15,8 +16,6 @@ import picocli.CommandLine;
|
|||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static io.kestra.core.utils.Rethrow.throwConsumer;
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "submit-queued-execution",
|
name = "submit-queued-execution",
|
||||||
description = {"Submit all queued execution to the executor",
|
description = {"Submit all queued execution to the executor",
|
||||||
@@ -49,9 +48,11 @@ public class SubmitQueuedCommand extends AbstractCommand {
|
|||||||
}
|
}
|
||||||
else if (queueType.get().equals("postgres") || queueType.get().equals("mysql") || queueType.get().equals("h2")) {
|
else if (queueType.get().equals("postgres") || queueType.get().equals("mysql") || queueType.get().equals("h2")) {
|
||||||
var executionQueuedStorage = applicationContext.getBean(AbstractJdbcExecutionQueuedStorage.class);
|
var executionQueuedStorage = applicationContext.getBean(AbstractJdbcExecutionQueuedStorage.class);
|
||||||
|
var concurrencyLimitService = applicationContext.getBean(ConcurrencyLimitService.class);
|
||||||
|
|
||||||
for (ExecutionQueued queued : executionQueuedStorage.getAllForAllTenants()) {
|
for (ExecutionQueued queued : executionQueuedStorage.getAllForAllTenants()) {
|
||||||
executionQueuedStorage.pop(queued.getTenantId(), queued.getNamespace(), queued.getFlowId(), throwConsumer(execution -> executionQueue.emit(execution.withState(State.Type.CREATED))));
|
Execution restart = concurrencyLimitService.unqueue(queued.getExecution(), State.Type.RUNNING);
|
||||||
|
executionQueue.emit(restart);
|
||||||
cpt++;
|
cpt++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class TemplateNamespaceUpdateCommand extends AbstractServiceNamespaceUpda
|
|||||||
|
|
||||||
try (DefaultHttpClient client = client()) {
|
try (DefaultHttpClient client = client()) {
|
||||||
MutableHttpRequest<List<Template>> request = HttpRequest
|
MutableHttpRequest<List<Template>> request = HttpRequest
|
||||||
.POST(apiUri("/templates/", tenantService.getTenantId(tenantId)) + namespace + "?delete=" + delete, templates);
|
.POST(apiUri("/templates/", tenantService.getTenantIdAndAllowEETenants(tenantId)) + namespace + "?delete=" + delete, templates);
|
||||||
|
|
||||||
List<UpdateResult> updated = client.toBlocking().retrieve(
|
List<UpdateResult> updated = client.toBlocking().retrieve(
|
||||||
this.requestOptions(request),
|
this.requestOptions(request),
|
||||||
|
|||||||
@@ -16,4 +16,11 @@ public class TenantIdSelectorService {
|
|||||||
}
|
}
|
||||||
return MAIN_TENANT;
|
return MAIN_TENANT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTenantIdAndAllowEETenants(String tenantId) {
|
||||||
|
if (StringUtils.isNotBlank(tenantId)){
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
return MAIN_TENANT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ micronaut:
|
|||||||
read-idle-timeout: 60m
|
read-idle-timeout: 60m
|
||||||
write-idle-timeout: 60m
|
write-idle-timeout: 60m
|
||||||
idle-timeout: 60m
|
idle-timeout: 60m
|
||||||
netty:
|
|
||||||
max-zstd-encode-size: 67108864 # increased to 64MB from the default of 32MB
|
|
||||||
max-chunk-size: 10MB
|
|
||||||
max-header-size: 32768 # increased from the default of 8k
|
|
||||||
responses:
|
responses:
|
||||||
file:
|
file:
|
||||||
cache-seconds: 86400
|
cache-seconds: 86400
|
||||||
cache-control:
|
cache-control:
|
||||||
public: true
|
public: true
|
||||||
|
netty:
|
||||||
|
max-zstd-encode-size: 67108864 # increased to 64MB from the default of 32MB
|
||||||
|
max-chunk-size: 10MB
|
||||||
|
max-header-size: 32768 # increased from the default of 8k
|
||||||
|
|
||||||
# Access log configuration, see https://docs.micronaut.io/latest/guide/index.html#accessLogger
|
# Access log configuration, see https://docs.micronaut.io/latest/guide/index.html#accessLogger
|
||||||
access-logger:
|
access-logger:
|
||||||
@@ -167,6 +167,9 @@ kestra:
|
|||||||
open-urls:
|
open-urls:
|
||||||
- "/ping"
|
- "/ping"
|
||||||
- "/api/v1/executions/webhook/"
|
- "/api/v1/executions/webhook/"
|
||||||
|
- "/api/v1/main/executions/webhook/"
|
||||||
|
- "/api/v1/*/executions/webhook/"
|
||||||
|
- "/api/v1/basicAuthValidationErrors"
|
||||||
|
|
||||||
preview:
|
preview:
|
||||||
initial-rows: 100
|
initial-rows: 100
|
||||||
|
|||||||
@@ -27,6 +27,26 @@ class FlowValidateCommandTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// github action kestra-io/validate-action requires being able to validate Flows from OSS CLI against a remote EE instance
|
||||||
|
void runForEEInstance() {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
System.setOut(new PrintStream(out));
|
||||||
|
|
||||||
|
try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
|
||||||
|
String[] args = {
|
||||||
|
"--tenant",
|
||||||
|
"some-ee-tenant",
|
||||||
|
"--local",
|
||||||
|
"src/test/resources/helper/include.yaml"
|
||||||
|
};
|
||||||
|
Integer call = PicocliRunner.call(FlowValidateCommand.class, ctx, args);
|
||||||
|
|
||||||
|
assertThat(call).isZero();
|
||||||
|
assertThat(out.toString()).contains("✓ - io.kestra.cli / include");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void warning() {
|
void warning() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-level marker interface for Kestra's plugin of type App.
|
* Top-level marker interface for Kestra's plugin of type App.
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +20,6 @@ public interface AppBlockInterface extends io.kestra.core.models.Plugin {
|
|||||||
)
|
)
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
String getType();
|
String getType();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-level marker interface for Kestra's plugin of type App.
|
* Top-level marker interface for Kestra's plugin of type App.
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +20,6 @@ public interface AppPluginInterface extends io.kestra.core.models.Plugin {
|
|||||||
)
|
)
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
String getType();
|
String getType();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.github.victools.jsonschema.generator.impl.DefinitionKey;
|
|||||||
import com.github.victools.jsonschema.generator.naming.DefaultSchemaDefinitionNamingStrategy;
|
import com.github.victools.jsonschema.generator.naming.DefaultSchemaDefinitionNamingStrategy;
|
||||||
import com.github.victools.jsonschema.module.jackson.JacksonModule;
|
import com.github.victools.jsonschema.module.jackson.JacksonModule;
|
||||||
import com.github.victools.jsonschema.module.jackson.JacksonOption;
|
import com.github.victools.jsonschema.module.jackson.JacksonOption;
|
||||||
|
import com.github.victools.jsonschema.module.jackson.JsonUnwrappedDefinitionProvider;
|
||||||
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
|
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
|
||||||
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
|
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
|
||||||
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
|
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
|
||||||
@@ -45,6 +46,9 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.lang.reflect.*;
|
import java.lang.reflect.*;
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
@@ -58,7 +62,9 @@ import static io.kestra.core.docs.AbstractClassDocumentation.required;
|
|||||||
import static io.kestra.core.serializers.JacksonMapper.MAP_TYPE_REFERENCE;
|
import static io.kestra.core.serializers.JacksonMapper.MAP_TYPE_REFERENCE;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@Slf4j
|
||||||
public class JsonSchemaGenerator {
|
public class JsonSchemaGenerator {
|
||||||
|
|
||||||
private static final List<Class<?>> TYPES_RESOLVED_AS_STRING = List.of(Duration.class, LocalTime.class, LocalDate.class, LocalDateTime.class, ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class);
|
private static final List<Class<?>> TYPES_RESOLVED_AS_STRING = List.of(Duration.class, LocalTime.class, LocalDate.class, LocalDateTime.class, ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class);
|
||||||
private static final List<Class<?>> SUBTYPE_RESOLUTION_EXCLUSION_FOR_PLUGIN_SCHEMA = List.of(Task.class, AbstractTrigger.class);
|
private static final List<Class<?>> SUBTYPE_RESOLUTION_EXCLUSION_FOR_PLUGIN_SCHEMA = List.of(Task.class, AbstractTrigger.class);
|
||||||
|
|
||||||
@@ -118,7 +124,7 @@ public class JsonSchemaGenerator {
|
|||||||
removeRequiredOnPropsWithDefaults(objectNode);
|
removeRequiredOnPropsWithDefaults(objectNode);
|
||||||
|
|
||||||
return MAPPER.convertValue(objectNode, MAP_TYPE_REFERENCE);
|
return MAPPER.convertValue(objectNode, MAP_TYPE_REFERENCE);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException("Unable to generate jsonschema for '" + cls.getName() + "'", e);
|
throw new IllegalArgumentException("Unable to generate jsonschema for '" + cls.getName() + "'", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,8 +276,22 @@ public class JsonSchemaGenerator {
|
|||||||
.with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
|
.with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
|
||||||
.with(Option.DEFINITION_FOR_MAIN_SCHEMA)
|
.with(Option.DEFINITION_FOR_MAIN_SCHEMA)
|
||||||
.with(Option.PLAIN_DEFINITION_KEYS)
|
.with(Option.PLAIN_DEFINITION_KEYS)
|
||||||
.with(Option.ALLOF_CLEANUP_AT_THE_END);;
|
.with(Option.ALLOF_CLEANUP_AT_THE_END);
|
||||||
|
|
||||||
|
// HACK: Registered a custom JsonUnwrappedDefinitionProvider prior to the JacksonModule
|
||||||
|
// to be able to return an CustomDefinition with an empty node when the ResolvedType can't be found.
|
||||||
|
builder.forTypesInGeneral().withCustomDefinitionProvider(new JsonUnwrappedDefinitionProvider(){
|
||||||
|
@Override
|
||||||
|
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
|
||||||
|
try {
|
||||||
|
return super.provideCustomSchemaDefinition(javaType, context);
|
||||||
|
} catch (NoClassDefFoundError e) {
|
||||||
|
// This error happens when a non-supported plugin type exists in the classpath.
|
||||||
|
log.debug("Cannot create schema definition for type '{}'. Cause: NoClassDefFoundError", javaType.getTypeName());
|
||||||
|
return new CustomDefinition(context.getGeneratorConfig().createObjectNode(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
if (!draft7) {
|
if (!draft7) {
|
||||||
builder.with(new JacksonModule(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM));
|
builder.with(new JacksonModule(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM));
|
||||||
} else {
|
} else {
|
||||||
@@ -300,6 +320,7 @@ public class JsonSchemaGenerator {
|
|||||||
// inline some type
|
// inline some type
|
||||||
builder.forTypesInGeneral()
|
builder.forTypesInGeneral()
|
||||||
.withCustomDefinitionProvider(new CustomDefinitionProviderV2() {
|
.withCustomDefinitionProvider(new CustomDefinitionProviderV2() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
|
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
|
||||||
if (javaType.isInstanceOf(Map.class) || javaType.isInstanceOf(Enum.class)) {
|
if (javaType.isInstanceOf(Map.class) || javaType.isInstanceOf(Enum.class)) {
|
||||||
|
|||||||
@@ -3,30 +3,88 @@ package io.kestra.core.events;
|
|||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import io.micronaut.http.HttpRequest;
|
import io.micronaut.http.HttpRequest;
|
||||||
import io.micronaut.http.context.ServerRequestContext;
|
import io.micronaut.http.context.ServerRequestContext;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@AllArgsConstructor
|
import java.util.Objects;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class CrudEvent<T> {
|
public class CrudEvent<T> {
|
||||||
T model;
|
private final T model;
|
||||||
@Nullable
|
@Nullable
|
||||||
T previousModel;
|
private final T previousModel;
|
||||||
CrudEventType type;
|
private final CrudEventType type;
|
||||||
HttpRequest<?> request;
|
private final HttpRequest<?> request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper method for creating a new {@link CrudEventType#UPDATE} CrudEvent.
|
||||||
|
*
|
||||||
|
* @param model the new created model.
|
||||||
|
* @param <T> type of the model.
|
||||||
|
* @return the new {@link CrudEvent}.
|
||||||
|
*/
|
||||||
|
public static <T> CrudEvent<T> create(T model) {
|
||||||
|
Objects.requireNonNull(model, "Can't create CREATE event with a null model");
|
||||||
|
return new CrudEvent<>(model, null, CrudEventType.CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper method for creating a new {@link CrudEventType#DELETE} CrudEvent.
|
||||||
|
*
|
||||||
|
* @param model the deleted model.
|
||||||
|
* @param <T> type of the model.
|
||||||
|
* @return the new {@link CrudEvent}.
|
||||||
|
*/
|
||||||
|
public static <T> CrudEvent<T> delete(T model) {
|
||||||
|
Objects.requireNonNull(model, "Can't create DELETE event with a null model");
|
||||||
|
return new CrudEvent<>(null, model, CrudEventType.DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper method for creating a new CrudEvent.
|
||||||
|
*
|
||||||
|
* @param before the model before the update.
|
||||||
|
* @param after the model after the update.
|
||||||
|
* @param <T> type of the model.
|
||||||
|
* @return the new {@link CrudEvent}.
|
||||||
|
*/
|
||||||
|
public static <T> CrudEvent<T> of(T before, T after) {
|
||||||
|
|
||||||
|
if (before == null && after == null) {
|
||||||
|
throw new IllegalArgumentException("Both before and after cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (before == null) {
|
||||||
|
return create(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (after == null) {
|
||||||
|
return delete(before);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CrudEvent<>(after, before, CrudEventType.UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use the static factory methods.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public CrudEvent(T model, CrudEventType type) {
|
public CrudEvent(T model, CrudEventType type) {
|
||||||
this.model = model;
|
this(
|
||||||
this.type = type;
|
CrudEventType.DELETE.equals(type) ? null : model,
|
||||||
this.previousModel = null;
|
CrudEventType.DELETE.equals(type) ? model : null,
|
||||||
this.request = ServerRequestContext.currentRequest().orElse(null);
|
type,
|
||||||
|
ServerRequestContext.currentRequest().orElse(null)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CrudEvent(T model, T previousModel, CrudEventType type) {
|
public CrudEvent(T model, T previousModel, CrudEventType type) {
|
||||||
|
this(model, previousModel, type, ServerRequestContext.currentRequest().orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CrudEvent(T model, T previousModel, CrudEventType type, HttpRequest<?> request) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.previousModel = previousModel;
|
this.previousModel = previousModel;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.request = ServerRequestContext.currentRequest().orElse(null);
|
this.request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package io.kestra.core.models;
|
package io.kestra.core.models;
|
||||||
|
|
||||||
import io.kestra.core.utils.MapUtils;
|
import io.kestra.core.utils.MapUtils;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public record Label(@NotNull String key, @NotNull String value) {
|
@Schema(description = "A key/value pair that can be attached to a Flow or Execution. Labels are often used to organize and categorize objects.")
|
||||||
|
public record Label(@NotEmpty String key, @NotEmpty String value) {
|
||||||
public static final String SYSTEM_PREFIX = "system.";
|
public static final String SYSTEM_PREFIX = "system.";
|
||||||
|
|
||||||
// system labels
|
// system labels
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
package io.kestra.core.models;
|
package io.kestra.core.models;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that can be implemented by classes supporting plugin versioning.
|
* Interface that can be implemented by classes supporting plugin versioning.
|
||||||
*
|
*
|
||||||
* @see Plugin
|
* @see Plugin
|
||||||
*/
|
*/
|
||||||
public interface PluginVersioning {
|
public interface PluginVersioning {
|
||||||
|
|
||||||
|
String TITLE = "Plugin Version";
|
||||||
|
String DESCRIPTION = """
|
||||||
|
Defines the version of the plugin to use.
|
||||||
|
|
||||||
@Pattern(regexp="\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?|([a-zA-Z0-9]+)")
|
The version must follow the Semantic Versioning (SemVer) specification:
|
||||||
@Schema(title = "The version of the plugin to use.")
|
- A single-digit MAJOR version (e.g., `1`).
|
||||||
|
- A MAJOR.MINOR version (e.g., `1.1`).
|
||||||
|
- A MAJOR.MINOR.PATCH version, optionally with any qualifier
|
||||||
|
(e.g., `1.1.2`, `1.1.0-SNAPSHOT`).
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
title = TITLE,
|
||||||
|
description = DESCRIPTION
|
||||||
|
)
|
||||||
String getVersion();
|
String getVersion();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import lombok.experimental.SuperBuilder;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@io.kestra.core.models.annotations.Plugin
|
@io.kestra.core.models.annotations.Plugin
|
||||||
@SuperBuilder
|
@SuperBuilder
|
||||||
@Getter
|
@Getter
|
||||||
@@ -20,6 +22,6 @@ import jakarta.validation.constraints.Pattern;
|
|||||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||||
public abstract class Condition implements Plugin, Rethrow.PredicateChecked<ConditionContext, InternalException> {
|
public abstract class Condition implements Plugin, Rethrow.PredicateChecked<ConditionContext, InternalException> {
|
||||||
@NotNull
|
@NotNull
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
protected String type;
|
protected String type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import io.kestra.core.models.annotations.Plugin;
|
|||||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||||
import io.kestra.plugin.core.dashboard.data.IData;
|
import io.kestra.plugin.core.dashboard.data.IData;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
@@ -20,6 +22,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -28,12 +32,14 @@ import java.util.Set;
|
|||||||
public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
|
public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
private Map<String, C> columns;
|
private Map<String, C> columns;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
|
@Valid
|
||||||
|
@Nullable
|
||||||
private List<AbstractFilter<F>> where;
|
private List<AbstractFilter<F>> where;
|
||||||
|
|
||||||
private List<OrderBy> orderBy;
|
private List<OrderBy> orderBy;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -27,7 +29,7 @@ import java.util.Set;
|
|||||||
public abstract class DataFilterKPI<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
|
public abstract class DataFilterKPI<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
private C columns;
|
private C columns;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -26,7 +28,7 @@ public abstract class Chart<P extends ChartOption> implements io.kestra.core.mod
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
protected String type;
|
protected String type;
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import io.kestra.core.utils.IdUtils;
|
|||||||
import io.kestra.core.utils.ListUtils;
|
import io.kestra.core.utils.ListUtils;
|
||||||
import io.kestra.core.utils.MapUtils;
|
import io.kestra.core.utils.MapUtils;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
@@ -77,10 +78,12 @@ public class Execution implements DeletedInterface, TenantInterface {
|
|||||||
|
|
||||||
@With
|
@With
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
@Schema(implementation = Object.class)
|
||||||
Map<String, Object> inputs;
|
Map<String, Object> inputs;
|
||||||
|
|
||||||
@With
|
@With
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
@Schema(implementation = Object.class)
|
||||||
Map<String, Object> outputs;
|
Map<String, Object> outputs;
|
||||||
|
|
||||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||||
@@ -88,6 +91,7 @@ public class Execution implements DeletedInterface, TenantInterface {
|
|||||||
List<Label> labels;
|
List<Label> labels;
|
||||||
|
|
||||||
@With
|
@With
|
||||||
|
@Schema(implementation = Object.class)
|
||||||
Map<String, Object> variables;
|
Map<String, Object> variables;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -272,7 +276,7 @@ public class Execution implements DeletedInterface, TenantInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Execution withTaskRun(TaskRun taskRun) throws InternalException {
|
public Execution withTaskRun(TaskRun taskRun) throws InternalException {
|
||||||
ArrayList<TaskRun> newTaskRunList = new ArrayList<>(this.taskRunList);
|
ArrayList<TaskRun> newTaskRunList = this.taskRunList == null ? new ArrayList<>() : new ArrayList<>(this.taskRunList);
|
||||||
|
|
||||||
boolean b = Collections.replaceAll(
|
boolean b = Collections.replaceAll(
|
||||||
newTaskRunList,
|
newTaskRunList,
|
||||||
@@ -936,7 +940,15 @@ public class Execution implements DeletedInterface, TenantInterface {
|
|||||||
for (TaskRun current : taskRuns) {
|
for (TaskRun current : taskRuns) {
|
||||||
if (!MapUtils.isEmpty(current.getOutputs())) {
|
if (!MapUtils.isEmpty(current.getOutputs())) {
|
||||||
if (current.getIteration() != null) {
|
if (current.getIteration() != null) {
|
||||||
taskOutputs = MapUtils.merge(taskOutputs, outputs(current, byIds));
|
Map<String, Object> merged = MapUtils.merge(taskOutputs, outputs(current, byIds));
|
||||||
|
// If one of two of the map is null in the merge() method, we just return the other
|
||||||
|
// And if the not null map is a Variables (= read only), we cast it back to a simple
|
||||||
|
// hashmap to avoid taskOutputs becoming read-only
|
||||||
|
// i.e this happen in nested loopUntil tasks
|
||||||
|
if (merged instanceof Variables) {
|
||||||
|
merged = new HashMap<>(merged);
|
||||||
|
}
|
||||||
|
taskOutputs = merged;
|
||||||
} else {
|
} else {
|
||||||
taskOutputs.putAll(outputs(current, byIds));
|
taskOutputs.putAll(outputs(current, byIds));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.Hidden;
|
|||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.event.Level;
|
import org.slf4j.event.Level;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
@@ -120,6 +121,16 @@ public class LogEntry implements DeletedInterface, TenantInterface {
|
|||||||
return logEntry.getTimestamp().toString() + " " + logEntry.getLevel() + " " + logEntry.getMessage();
|
return logEntry.getTimestamp().toString() + " " + logEntry.getLevel() + " " + logEntry.getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String toPrettyString(LogEntry logEntry, Integer maxMessageSize) {
|
||||||
|
String message;
|
||||||
|
if (maxMessageSize != null && maxMessageSize > 0) {
|
||||||
|
message = StringUtils.truncate(logEntry.getMessage(), maxMessageSize);
|
||||||
|
} else {
|
||||||
|
message = logEntry.getMessage();
|
||||||
|
}
|
||||||
|
return logEntry.getTimestamp().toString() + " " + logEntry.getLevel() + " " + message;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, String> toMap() {
|
public Map<String, String> toMap() {
|
||||||
return Stream
|
return Stream
|
||||||
.of(
|
.of(
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package io.kestra.core.models.executions;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import io.kestra.core.models.TenantInterface;
|
import io.kestra.core.models.TenantInterface;
|
||||||
import io.kestra.core.models.flows.State;
|
import io.kestra.core.models.flows.State;
|
||||||
|
import io.kestra.core.models.tasks.FlowableTask;
|
||||||
import io.kestra.core.models.tasks.ResolvedTask;
|
import io.kestra.core.models.tasks.ResolvedTask;
|
||||||
|
import io.kestra.core.models.tasks.Task;
|
||||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||||
import io.kestra.core.utils.IdUtils;
|
import io.kestra.core.utils.IdUtils;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
@@ -52,6 +55,8 @@ public class TaskRun implements TenantInterface {
|
|||||||
|
|
||||||
@With
|
@With
|
||||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||||
|
@Nullable
|
||||||
|
@Schema(implementation = Object.class)
|
||||||
Variables outputs;
|
Variables outputs;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -64,7 +69,6 @@ public class TaskRun implements TenantInterface {
|
|||||||
Boolean dynamic;
|
Boolean dynamic;
|
||||||
|
|
||||||
// Set it to true to force execution even if the execution is killed
|
// Set it to true to force execution even if the execution is killed
|
||||||
@Nullable
|
|
||||||
@With
|
@With
|
||||||
Boolean forceExecution;
|
Boolean forceExecution;
|
||||||
|
|
||||||
@@ -217,7 +221,7 @@ public class TaskRun implements TenantInterface {
|
|||||||
public boolean isSame(TaskRun taskRun) {
|
public boolean isSame(TaskRun taskRun) {
|
||||||
return this.getId().equals(taskRun.getId()) &&
|
return this.getId().equals(taskRun.getId()) &&
|
||||||
((this.getValue() == null && taskRun.getValue() == null) || (this.getValue() != null && this.getValue().equals(taskRun.getValue()))) &&
|
((this.getValue() == null && taskRun.getValue() == null) || (this.getValue() != null && this.getValue().equals(taskRun.getValue()))) &&
|
||||||
((this.getIteration() == null && taskRun.getIteration() == null) || (this.getIteration() != null && this.getIteration().equals(taskRun.getIteration()))) ;
|
((this.getIteration() == null && taskRun.getIteration() == null) || (this.getIteration() != null && this.getIteration().equals(taskRun.getIteration())));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString(boolean pretty) {
|
public String toString(boolean pretty) {
|
||||||
@@ -249,7 +253,7 @@ public class TaskRun implements TenantInterface {
|
|||||||
* This method is used when the retry is apply on a task
|
* This method is used when the retry is apply on a task
|
||||||
* but the retry type is NEW_EXECUTION
|
* but the retry type is NEW_EXECUTION
|
||||||
*
|
*
|
||||||
* @param retry Contains the retry configuration
|
* @param retry Contains the retry configuration
|
||||||
* @param execution Contains the attempt number and original creation date
|
* @param execution Contains the attempt number and original creation date
|
||||||
* @return The next retry date, null if maxAttempt || maxDuration is reached
|
* @return The next retry date, null if maxAttempt || maxDuration is reached
|
||||||
*/
|
*/
|
||||||
@@ -270,6 +274,7 @@ public class TaskRun implements TenantInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is used when the Retry definition comes from the flow
|
* This method is used when the Retry definition comes from the flow
|
||||||
|
*
|
||||||
* @param retry The retry configuration
|
* @param retry The retry configuration
|
||||||
* @return The next retry date, null if maxAttempt || maxDuration is reached
|
* @return The next retry date, null if maxAttempt || maxDuration is reached
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package io.kestra.core.models.flows;
|
|||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.kestra.core.models.Label;
|
import io.kestra.core.models.Label;
|
||||||
import io.kestra.core.models.annotations.PluginProperty;
|
|
||||||
import io.kestra.core.models.tasks.WorkerGroup;
|
import io.kestra.core.models.tasks.WorkerGroup;
|
||||||
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
|
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
|
||||||
import io.kestra.core.serializers.ListOrMapOfLabelSerializer;
|
import io.kestra.core.serializers.ListOrMapOfLabelSerializer;
|
||||||
@@ -61,12 +60,24 @@ public abstract class AbstractFlow implements FlowInterface {
|
|||||||
|
|
||||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||||
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
|
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
|
||||||
@Schema(implementation = Object.class, oneOf = {List.class, Map.class})
|
@Schema(
|
||||||
|
description = "Labels as a list of Label (key/value pairs) or as a map of string to string.",
|
||||||
|
oneOf = {
|
||||||
|
Label[].class,
|
||||||
|
Map.class
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@Valid
|
||||||
List<Label> labels;
|
List<Label> labels;
|
||||||
|
|
||||||
@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE)
|
@Schema(
|
||||||
|
type = "object",
|
||||||
|
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
|
||||||
|
)
|
||||||
Map<String, Object> variables;
|
Map<String, Object> variables;
|
||||||
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
private WorkerGroup workerGroup;
|
private WorkerGroup workerGroup;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ public class Flow extends AbstractFlow implements HasUID {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
type = "object",
|
||||||
|
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
|
||||||
|
)
|
||||||
Map<String, Object> variables;
|
Map<String, Object> variables;
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
|||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
import io.kestra.core.models.flows.input.*;
|
import io.kestra.core.models.flows.input.*;
|
||||||
import io.kestra.core.models.property.Property;
|
import io.kestra.core.models.property.Property;
|
||||||
import io.kestra.core.runners.RunContext;
|
|
||||||
import io.micronaut.core.annotation.Introspected;
|
import io.micronaut.core.annotation.Introspected;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
@@ -18,8 +17,6 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@SuperBuilder
|
@SuperBuilder
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.kestra.core.models.flows;
|
package io.kestra.core.models.flows;
|
||||||
|
|
||||||
import io.micronaut.core.annotation.Introspected;
|
import io.micronaut.core.annotation.Introspected;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -33,6 +34,12 @@ public class Output implements Data {
|
|||||||
* The output value. Can be a dynamic expression.
|
* The output value. Can be a dynamic expression.
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@Schema(
|
||||||
|
oneOf = {
|
||||||
|
Object.class,
|
||||||
|
String.class
|
||||||
|
}
|
||||||
|
)
|
||||||
Object value;
|
Object value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.kestra.core.models.flows;
|
|||||||
|
|
||||||
import io.kestra.core.validations.PluginDefaultValidation;
|
import io.kestra.core.validations.PluginDefaultValidation;
|
||||||
import io.micronaut.core.annotation.Introspected;
|
import io.micronaut.core.annotation.Introspected;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -21,6 +22,10 @@ public class PluginDefault {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private final boolean forced = false;
|
private final boolean forced = false;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
type = "object",
|
||||||
|
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
|
||||||
|
)
|
||||||
private final Map<String, Object> values;
|
private final Map<String, Object> values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.google.common.annotations.VisibleForTesting;
|
|||||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||||
import io.kestra.core.runners.RunContext;
|
import io.kestra.core.runners.RunContext;
|
||||||
import io.kestra.core.serializers.JacksonMapper;
|
import io.kestra.core.serializers.JacksonMapper;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -34,8 +35,13 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
|
|||||||
@JsonDeserialize(using = Property.PropertyDeserializer.class)
|
@JsonDeserialize(using = Property.PropertyDeserializer.class)
|
||||||
@JsonSerialize(using = Property.PropertySerializer.class)
|
@JsonSerialize(using = Property.PropertySerializer.class)
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor(access = AccessLevel.PACKAGE)
|
@AllArgsConstructor(access = AccessLevel.PACKAGE)
|
||||||
|
@Schema(
|
||||||
|
oneOf = {
|
||||||
|
Object.class,
|
||||||
|
String.class
|
||||||
|
}
|
||||||
|
)
|
||||||
public class Property<T> {
|
public class Property<T> {
|
||||||
// By default, durations are stored as numbers.
|
// By default, durations are stored as numbers.
|
||||||
// We cannot change that globally, as in JDBC/Elastic 'execution.state.duration' must be a number to be able to aggregate them.
|
// We cannot change that globally, as in JDBC/Elastic 'execution.state.duration' must be a number to be able to aggregate them.
|
||||||
@@ -44,6 +50,7 @@ public class Property<T> {
|
|||||||
.copy()
|
.copy()
|
||||||
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
|
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
|
||||||
|
|
||||||
|
private final boolean skipCache;
|
||||||
private String expression;
|
private String expression;
|
||||||
private T value;
|
private T value;
|
||||||
|
|
||||||
@@ -53,13 +60,23 @@ public class Property<T> {
|
|||||||
@Deprecated
|
@Deprecated
|
||||||
// Note: when not used, this constructor would not be deleted but made private so it can only be used by ofExpression(String) and the deserializer
|
// Note: when not used, this constructor would not be deleted but made private so it can only be used by ofExpression(String) and the deserializer
|
||||||
public Property(String expression) {
|
public Property(String expression) {
|
||||||
this.expression = expression;
|
this(expression, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Property(String expression, boolean skipCache) {
|
||||||
|
this.expression = expression;
|
||||||
|
this.skipCache = skipCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use {@link #ofValue(Object)} instead.
|
||||||
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@Deprecated
|
||||||
public Property(Map<?, ?> map) {
|
public Property(Map<?, ?> map) {
|
||||||
try {
|
try {
|
||||||
expression = MAPPER.writeValueAsString(map);
|
expression = MAPPER.writeValueAsString(map);
|
||||||
|
this.skipCache = false;
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalArgumentException(e);
|
throw new IllegalArgumentException(e);
|
||||||
}
|
}
|
||||||
@@ -68,13 +85,10 @@ public class Property<T> {
|
|||||||
String getExpression() {
|
String getExpression() {
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new {@link Property} with no cached rendered value,
|
* Returns a new {@link Property} with no cached rendered value,
|
||||||
* so that the next render will evaluate its original Pebble expression.
|
* so that the next render will evaluate its original Pebble expression.
|
||||||
* <p>
|
|
||||||
* The returned property will still cache its rendered result.
|
|
||||||
* To re-evaluate on a subsequent render, call {@code skipCache()} again.
|
|
||||||
*
|
*
|
||||||
* @return a new {@link Property} without a pre-rendered value
|
* @return a new {@link Property} without a pre-rendered value
|
||||||
*/
|
*/
|
||||||
@@ -84,9 +98,9 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a new Property object with a value already set.<br>
|
* Build a new Property object with a value already set.<br>
|
||||||
*
|
* <p>
|
||||||
* A property build with this method will always return the value passed at build time, no rendering will be done.
|
* A property build with this method will always return the value passed at build time, no rendering will be done.
|
||||||
*
|
* <p>
|
||||||
* Use {@link #ofExpression(String)} to build a property with a Pebble expression instead.
|
* Use {@link #ofExpression(String)} to build a property with a Pebble expression instead.
|
||||||
*/
|
*/
|
||||||
public static <V> Property<V> ofValue(V value) {
|
public static <V> Property<V> ofValue(V value) {
|
||||||
@@ -126,21 +140,22 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a new Property object with a Pebble expression.<br>
|
* Build a new Property object with a Pebble expression.<br>
|
||||||
*
|
* This property object will not cache its rendered value.
|
||||||
|
* <p>
|
||||||
* Use {@link #ofValue(Object)} to build a property with a value instead.
|
* Use {@link #ofValue(Object)} to build a property with a value instead.
|
||||||
*/
|
*/
|
||||||
public static <V> Property<V> ofExpression(@NotNull String expression) {
|
public static <V> Property<V> ofExpression(@NotNull String expression) {
|
||||||
Objects.requireNonNull(expression, "'expression' is required");
|
Objects.requireNonNull(expression, "'expression' is required");
|
||||||
if(!expression.contains("{")) {
|
if (!expression.contains("{")) {
|
||||||
throw new IllegalArgumentException("'expression' must be a valid Pebble expression");
|
throw new IllegalArgumentException("'expression' must be a valid Pebble expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Property<>(expression);
|
return new Property<>(expression, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property then convert it to its target type.<br>
|
* Render a property, then convert it to its target type.<br>
|
||||||
*
|
* <p>
|
||||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||||
*
|
*
|
||||||
* @see io.kestra.core.runners.RunContextProperty#as(Class)
|
* @see io.kestra.core.runners.RunContextProperty#as(Class)
|
||||||
@@ -151,14 +166,14 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property with additional variables, then convert it to its target type.<br>
|
* Render a property with additional variables, then convert it to its target type.<br>
|
||||||
*
|
* <p>
|
||||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||||
*
|
*
|
||||||
* @see io.kestra.core.runners.RunContextProperty#as(Class, Map)
|
* @see io.kestra.core.runners.RunContextProperty#as(Class, Map)
|
||||||
*/
|
*/
|
||||||
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
if (property.value == null) {
|
if (property.skipCache || property.value == null) {
|
||||||
String rendered = context.render(property.expression, variables);
|
String rendered = context.render(property.expression, variables);
|
||||||
property.value = MAPPER.convertValue(rendered, clazz);
|
property.value = MAPPER.convertValue(rendered, clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +182,7 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property then convert it as a list of target type.<br>
|
* Render a property then convert it as a list of target type.<br>
|
||||||
*
|
* <p>
|
||||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||||
*
|
*
|
||||||
* @see io.kestra.core.runners.RunContextProperty#asList(Class)
|
* @see io.kestra.core.runners.RunContextProperty#asList(Class)
|
||||||
@@ -178,14 +193,14 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property with additional variables, then convert it as a list of target type.<br>
|
* Render a property with additional variables, then convert it as a list of target type.<br>
|
||||||
*
|
* <p>
|
||||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||||
*
|
*
|
||||||
* @see io.kestra.core.runners.RunContextProperty#asList(Class, Map)
|
* @see io.kestra.core.runners.RunContextProperty#asList(Class, Map)
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T, I> T asList(Property<T> property, PropertyContext context, Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
public static <T, I> T asList(Property<T> property, PropertyContext context, Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
if (property.value == null) {
|
if (property.skipCache || property.value == null) {
|
||||||
JavaType type = MAPPER.getTypeFactory().constructCollectionLikeType(List.class, itemClazz);
|
JavaType type = MAPPER.getTypeFactory().constructCollectionLikeType(List.class, itemClazz);
|
||||||
try {
|
try {
|
||||||
String trimmedExpression = property.expression.trim();
|
String trimmedExpression = property.expression.trim();
|
||||||
@@ -218,26 +233,26 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property then convert it as a map of target types.<br>
|
* Render a property then convert it as a map of target types.<br>
|
||||||
*
|
* <p>
|
||||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||||
*
|
*
|
||||||
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class)
|
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class)
|
||||||
*/
|
*/
|
||||||
public static <T, K,V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
|
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
|
||||||
return asMap(property, runContext, keyClass, valueClass, Map.of());
|
return asMap(property, runContext, keyClass, valueClass, Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property with additional variables, then convert it as a map of target types.<br>
|
* Render a property with additional variables, then convert it as a map of target types.<br>
|
||||||
*
|
* <p>
|
||||||
* This method is safe to be used as many times as you want as the rendering and conversion will be cached.
|
* This method is safe to be used as many times as you want as the rendering and conversion will be cached.
|
||||||
* Warning, due to the caching mechanism, this method is not thread-safe.
|
* Warning, due to the caching mechanism, this method is not thread-safe.
|
||||||
*
|
*
|
||||||
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class, Map)
|
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class, Map)
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
public static <T, K,V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
if (property.value == null) {
|
if (property.skipCache || property.value == null) {
|
||||||
JavaType targetMapType = MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass);
|
JavaType targetMapType = MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||||
public interface TaskInterface extends Plugin, PluginVersioning {
|
public interface TaskInterface extends Plugin, PluginVersioning {
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -17,7 +19,7 @@ public interface TaskInterface extends Plugin, PluginVersioning {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
@Schema(title = "The class name of this task.")
|
@Schema(title = "The class name of this task.")
|
||||||
String getType();
|
String getType();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@Plugin
|
@Plugin
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@@ -22,7 +24,7 @@ public abstract class LogExporter<T extends Output> implements io.kestra.core.m
|
|||||||
protected String id;
|
protected String id;
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
protected String type;
|
protected String type;
|
||||||
|
|
||||||
public abstract T sendLogs(RunContext runContext, Flux<LogRecord> logRecords) throws Exception;
|
public abstract T sendLogs(RunContext runContext, Flux<LogRecord> logRecords) throws Exception;
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ public final class LogRecordMapper {
|
|||||||
private LogRecordMapper(){}
|
private LogRecordMapper(){}
|
||||||
|
|
||||||
public static LogRecord mapToLogRecord(LogEntry log) {
|
public static LogRecord mapToLogRecord(LogEntry log) {
|
||||||
|
return mapToLogRecord(log, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogRecord mapToLogRecord(LogEntry log, Integer maxMessageSize) {
|
||||||
return LogRecord.builder()
|
return LogRecord.builder()
|
||||||
.resource("Kestra")
|
.resource("Kestra")
|
||||||
.timestampEpochNanos(instantInNanos(log.getTimestamp()))
|
.timestampEpochNanos(instantInNanos(log.getTimestamp()))
|
||||||
.severity(log.getLevel().name())
|
.severity(log.getLevel().name())
|
||||||
.attributes(log.toLogMap())
|
.attributes(log.toLogMap())
|
||||||
.bodyValue(LogEntry.toPrettyString(log))
|
.bodyValue(LogEntry.toPrettyString(log, maxMessageSize))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
package io.kestra.core.models.tasks.runners;
|
package io.kestra.core.models.tasks.runners;
|
||||||
|
|
||||||
public interface RemoteRunnerInterface {}
|
import io.kestra.core.models.property.Property;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
public interface RemoteRunnerInterface {
|
||||||
|
@Schema(
|
||||||
|
title = "Whether to synchronize working directory from remote runner back to local one after run."
|
||||||
|
)
|
||||||
|
Property<Boolean> getSyncWorkingDirectory();
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ public interface TaskCommands {
|
|||||||
|
|
||||||
Map<String, Object> getAdditionalVars();
|
Map<String, Object> getAdditionalVars();
|
||||||
|
|
||||||
|
default String outputDirectoryName() {
|
||||||
|
return this.getWorkingDirectory().relativize(this.getOutputDirectory()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
Path getWorkingDirectory();
|
Path getWorkingDirectory();
|
||||||
|
|
||||||
Path getOutputDirectory();
|
Path getOutputDirectory();
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import io.kestra.core.models.Plugin;
|
|||||||
import io.kestra.core.models.PluginVersioning;
|
import io.kestra.core.models.PluginVersioning;
|
||||||
import io.kestra.core.models.WorkerJobLifecycle;
|
import io.kestra.core.models.WorkerJobLifecycle;
|
||||||
import io.kestra.core.models.annotations.PluginProperty;
|
import io.kestra.core.models.annotations.PluginProperty;
|
||||||
import io.kestra.core.models.property.Property;
|
|
||||||
import io.kestra.core.runners.RunContext;
|
import io.kestra.core.runners.RunContext;
|
||||||
import io.kestra.plugin.core.runner.Process;
|
import io.kestra.plugin.core.runner.Process;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@@ -19,13 +18,14 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.util.HashMap;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
|
import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all task runners.
|
* Base class for all task runners.
|
||||||
@@ -37,7 +37,7 @@ import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
|
|||||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||||
public abstract class TaskRunner<T extends TaskRunnerDetailResult> implements Plugin, PluginVersioning, WorkerJobLifecycle {
|
public abstract class TaskRunner<T extends TaskRunnerDetailResult> implements Plugin, PluginVersioning, WorkerJobLifecycle {
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
protected String type;
|
protected String type;
|
||||||
|
|
||||||
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
|
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ abstract public class AbstractTrigger implements TriggerInterface {
|
|||||||
@Valid
|
@Valid
|
||||||
protected List<@Valid @NotNull Condition> conditions;
|
protected List<@Valid @NotNull Condition> conditions;
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
|
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
|
||||||
|
@Schema(defaultValue = "false")
|
||||||
private boolean disabled = false;
|
private boolean disabled = false;
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
|
|||||||
@@ -185,34 +185,6 @@ public class Trigger extends TriggerContext implements HasUID {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Trigger update(Trigger currentTrigger, Trigger newTrigger, ZonedDateTime nextExecutionDate) throws Exception {
|
|
||||||
Trigger updated = currentTrigger;
|
|
||||||
|
|
||||||
// If a backfill is created, we update the currentTrigger
|
|
||||||
// and set the nextExecutionDate() as the previous one
|
|
||||||
if (newTrigger.getBackfill() != null) {
|
|
||||||
updated = currentTrigger.toBuilder()
|
|
||||||
.backfill(
|
|
||||||
newTrigger
|
|
||||||
.getBackfill()
|
|
||||||
.toBuilder()
|
|
||||||
.end(newTrigger.getBackfill().getEnd() != null ? newTrigger.getBackfill().getEnd() : ZonedDateTime.now())
|
|
||||||
.currentDate(
|
|
||||||
newTrigger.getBackfill().getStart()
|
|
||||||
)
|
|
||||||
.previousNextExecutionDate(
|
|
||||||
currentTrigger.getNextExecutionDate())
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated.toBuilder()
|
|
||||||
.nextExecutionDate(newTrigger.getDisabled() ?
|
|
||||||
null : nextExecutionDate)
|
|
||||||
.disabled(newTrigger.getDisabled())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Trigger resetExecution(Flow flow, Execution execution, ConditionContext conditionContext) {
|
public Trigger resetExecution(Flow flow, Execution execution, ConditionContext conditionContext) {
|
||||||
boolean disabled = this.getStopAfter() != null ? this.getStopAfter().contains(execution.getState().getCurrent()) : this.getDisabled();
|
boolean disabled = this.getStopAfter() != null ? this.getStopAfter().contains(execution.getState().getCurrent()) : this.getDisabled();
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
@@ -276,27 +248,22 @@ public class Trigger extends TriggerContext implements HasUID {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Trigger initBackfill(Trigger newTrigger) {
|
public Trigger withBackfill(final Backfill backfill) {
|
||||||
// If a backfill is created, we update the currentTrigger
|
Trigger updated = this;
|
||||||
|
// If a backfill is created, we update the trigger
|
||||||
// and set the nextExecutionDate() as the previous one
|
// and set the nextExecutionDate() as the previous one
|
||||||
if (newTrigger.getBackfill() != null) {
|
if (backfill != null) {
|
||||||
|
updated = this.toBuilder()
|
||||||
return this.toBuilder()
|
|
||||||
.backfill(
|
.backfill(
|
||||||
newTrigger
|
backfill
|
||||||
.getBackfill()
|
|
||||||
.toBuilder()
|
.toBuilder()
|
||||||
.end(newTrigger.getBackfill().getEnd() != null ? newTrigger.getBackfill().getEnd() : ZonedDateTime.now())
|
.end(backfill.getEnd() != null ? backfill.getEnd() : ZonedDateTime.now())
|
||||||
.currentDate(
|
.currentDate(backfill.getStart())
|
||||||
newTrigger.getBackfill().getStart()
|
.previousNextExecutionDate(this.getNextExecutionDate())
|
||||||
)
|
|
||||||
.previousNextExecutionDate(
|
|
||||||
this.getNextExecutionDate())
|
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
return updated;
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the next date is after the backfill end, we remove the backfill
|
// if the next date is after the backfill end, we remove the backfill
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.flows.State;
|
|||||||
import io.kestra.core.utils.IdUtils;
|
import io.kestra.core.utils.IdUtils;
|
||||||
import io.micronaut.core.annotation.Introspected;
|
import io.micronaut.core.annotation.Introspected;
|
||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -46,6 +47,7 @@ public class TriggerContext {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private List<State.Type> stopAfter;
|
private List<State.Type> stopAfter;
|
||||||
|
|
||||||
|
@Schema(defaultValue = "false")
|
||||||
private Boolean disabled = Boolean.FALSE;
|
private Boolean disabled = Boolean.FALSE;
|
||||||
|
|
||||||
protected TriggerContext(TriggerContextBuilder<?, ?> b) {
|
protected TriggerContext(TriggerContextBuilder<?, ?> b) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
public interface TriggerInterface extends Plugin, PluginVersioning {
|
public interface TriggerInterface extends Plugin, PluginVersioning {
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -17,7 +18,7 @@ public interface TriggerInterface extends Plugin, PluginVersioning {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
@Schema(title = "The class name for this current trigger.")
|
@Schema(title = "The class name for this current trigger.")
|
||||||
String getType();
|
String getType();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
import static io.kestra.core.utils.RegexPatterns.JAVA_IDENTIFIER_REGEX;
|
||||||
|
|
||||||
@io.kestra.core.models.annotations.Plugin
|
@io.kestra.core.models.annotations.Plugin
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@@ -15,6 +17,6 @@ import lombok.experimental.SuperBuilder;
|
|||||||
public abstract class AdditionalPlugin implements Plugin {
|
public abstract class AdditionalPlugin implements Plugin {
|
||||||
@NotNull
|
@NotNull
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
protected String type;
|
protected String type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import lombok.Getter;
|
|||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@@ -14,5 +20,59 @@ import java.net.URL;
|
|||||||
public class ExternalPlugin {
|
public class ExternalPlugin {
|
||||||
private final URL location;
|
private final URL location;
|
||||||
private final URL[] resources;
|
private final URL[] resources;
|
||||||
private final long crc32;
|
private volatile Long crc32; // lazy-val
|
||||||
|
|
||||||
|
public ExternalPlugin(URL location, URL[] resources) {
|
||||||
|
this.location = location;
|
||||||
|
this.resources = resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCrc32() {
|
||||||
|
if (this.crc32 == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (this.crc32 == null) {
|
||||||
|
this.crc32 = computeJarCrc32(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a CRC32 of the JAR File without reading the whole file
|
||||||
|
*
|
||||||
|
* @param location of the JAR File.
|
||||||
|
* @return the CRC32 of {@code -1} if the checksum can't be computed.
|
||||||
|
*/
|
||||||
|
private static long computeJarCrc32(final URL location) {
|
||||||
|
CRC32 crc = new CRC32();
|
||||||
|
try (JarFile jar = new JarFile(location.toURI().getPath(), false)) {
|
||||||
|
Enumeration<JarEntry> entries = jar.entries();
|
||||||
|
byte[] buffer = new byte[Long.BYTES]; // reusable buffer to avoid re-allocation
|
||||||
|
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
JarEntry entry = entries.nextElement();
|
||||||
|
crc.update(entry.getName().getBytes(StandardCharsets.UTF_8));
|
||||||
|
updateCrc32WithLong(crc, buffer, entry.getSize());
|
||||||
|
updateCrc32WithLong(crc, buffer, entry.getCrc());
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc.getValue();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateCrc32WithLong(CRC32 crc32, byte[] reusable, long val) {
|
||||||
|
// fast long -> byte conversion
|
||||||
|
reusable[0] = (byte) (val >>> 56);
|
||||||
|
reusable[1] = (byte) (val >>> 48);
|
||||||
|
reusable[2] = (byte) (val >>> 40);
|
||||||
|
reusable[3] = (byte) (val >>> 32);
|
||||||
|
reusable[4] = (byte) (val >>> 24);
|
||||||
|
reusable[5] = (byte) (val >>> 16);
|
||||||
|
reusable[6] = (byte) (val >>> 8);
|
||||||
|
reusable[7] = (byte) val;
|
||||||
|
crc32.update(reusable);;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.kestra.core.plugins;
|
package io.kestra.core.plugins;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -33,7 +34,7 @@ public record PluginArtifact(
|
|||||||
String version,
|
String version,
|
||||||
URI uri
|
URI uri
|
||||||
) implements Comparable<PluginArtifact> {
|
) implements Comparable<PluginArtifact> {
|
||||||
|
|
||||||
private static final Pattern ARTIFACT_PATTERN = Pattern.compile(
|
private static final Pattern ARTIFACT_PATTERN = Pattern.compile(
|
||||||
"([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"
|
"([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"
|
||||||
);
|
);
|
||||||
@@ -42,7 +43,8 @@ public record PluginArtifact(
|
|||||||
);
|
);
|
||||||
|
|
||||||
public static final String JAR_EXTENSION = "jar";
|
public static final String JAR_EXTENSION = "jar";
|
||||||
|
public static final String KESTRA_GROUP_ID = "io.kestra";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static helper method for constructing a new {@link PluginArtifact} from a JAR file.
|
* Static helper method for constructing a new {@link PluginArtifact} from a JAR file.
|
||||||
*
|
*
|
||||||
@@ -135,6 +137,11 @@ public record PluginArtifact(
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return toCoordinates();
|
return toCoordinates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isOfficial() {
|
||||||
|
return groupId.startsWith(KESTRA_GROUP_ID);
|
||||||
|
}
|
||||||
|
|
||||||
public String toCoordinates() {
|
public String toCoordinates() {
|
||||||
return Stream.of(groupId, artifactId, extension, classifier, version)
|
return Stream.of(groupId, artifactId, extension, classifier, version)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package io.kestra.core.plugins;
|
package io.kestra.core.plugins;
|
||||||
|
|
||||||
|
import io.kestra.core.contexts.KestraContext;
|
||||||
|
import io.kestra.core.utils.ListUtils;
|
||||||
|
import io.kestra.core.utils.Version;
|
||||||
import io.micronaut.core.type.Argument;
|
import io.micronaut.core.type.Argument;
|
||||||
import io.micronaut.http.HttpMethod;
|
import io.micronaut.http.HttpMethod;
|
||||||
import io.micronaut.http.HttpRequest;
|
import io.micronaut.http.HttpRequest;
|
||||||
import io.micronaut.http.HttpResponse;
|
import io.micronaut.http.HttpResponse;
|
||||||
|
import io.micronaut.http.MutableHttpRequest;
|
||||||
import io.micronaut.http.client.HttpClient;
|
import io.micronaut.http.client.HttpClient;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -15,9 +19,12 @@ import java.util.Base64;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Services for retrieving available plugin artifacts for Kestra.
|
* Services for retrieving available plugin artifacts for Kestra.
|
||||||
@@ -39,6 +46,8 @@ public class PluginCatalogService {
|
|||||||
|
|
||||||
private final boolean icons;
|
private final boolean icons;
|
||||||
private final boolean oss;
|
private final boolean oss;
|
||||||
|
|
||||||
|
private final Version currentStableVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link PluginCatalogService} instance.
|
* Creates a new {@link PluginCatalogService} instance.
|
||||||
@@ -53,11 +62,55 @@ public class PluginCatalogService {
|
|||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
this.icons = icons;
|
this.icons = icons;
|
||||||
this.oss = communityOnly;
|
this.oss = communityOnly;
|
||||||
|
|
||||||
|
Version version = Version.of(KestraContext.getContext().getVersion());
|
||||||
|
this.currentStableVersion = new Version(version.majorVersion(), version.minorVersion(), version.patchVersion(), null);
|
||||||
|
|
||||||
// Immediately trigger an async load of plugin artifacts.
|
// Immediately trigger an async load of plugin artifacts.
|
||||||
this.isLoaded.set(true);
|
this.isLoaded.set(true);
|
||||||
this.plugins = CompletableFuture.supplyAsync(this::load);
|
this.plugins = CompletableFuture.supplyAsync(this::load);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the version for the given artifacts.
|
||||||
|
*
|
||||||
|
* @param artifacts The list of artifacts to resolve.
|
||||||
|
* @return The list of results.
|
||||||
|
*/
|
||||||
|
public List<PluginResolutionResult> resolveVersions(List<PluginArtifact> artifacts) {
|
||||||
|
if (ListUtils.isEmpty(artifacts)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, ApiPluginArtifact> pluginsByGroupAndArtifactId = getAllCompatiblePlugins().stream()
|
||||||
|
.collect(Collectors.toMap(it -> it.groupId() + ":" + it.artifactId(), Function.identity()));
|
||||||
|
|
||||||
|
return artifacts.stream().map(it -> {
|
||||||
|
// Get all compatible versions for current artifact
|
||||||
|
List<String> versions = Optional
|
||||||
|
.ofNullable(pluginsByGroupAndArtifactId.get(it.groupId() + ":" + it.artifactId()))
|
||||||
|
.map(ApiPluginArtifact::versions)
|
||||||
|
.orElse(List.of());
|
||||||
|
|
||||||
|
// Try to resolve the version
|
||||||
|
String resolvedVersion = null;
|
||||||
|
if (!versions.isEmpty()) {
|
||||||
|
if (it.version().equalsIgnoreCase("LATEST")) {
|
||||||
|
resolvedVersion = versions.getFirst();
|
||||||
|
} else {
|
||||||
|
resolvedVersion = versions.contains(it.version()) ? it.version() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the PluginResolutionResult
|
||||||
|
return new PluginResolutionResult(
|
||||||
|
it,
|
||||||
|
resolvedVersion,
|
||||||
|
versions,
|
||||||
|
resolvedVersion != null
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized List<PluginManifest> get() {
|
public synchronized List<PluginManifest> get() {
|
||||||
try {
|
try {
|
||||||
@@ -140,7 +193,27 @@ public class PluginCatalogService {
|
|||||||
isLoaded.set(false);
|
isLoaded.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<ApiPluginArtifact> getAllCompatiblePlugins() {
|
||||||
|
|
||||||
|
MutableHttpRequest<Object> request = HttpRequest.create(
|
||||||
|
HttpMethod.GET,
|
||||||
|
"/v1/plugins/artifacts/core-compatibility/" + currentStableVersion
|
||||||
|
);
|
||||||
|
if (oss) {
|
||||||
|
request.getParameters().add("license", "OPENSOURCE");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return httpClient
|
||||||
|
.toBlocking()
|
||||||
|
.exchange(request, Argument.listOf(ApiPluginArtifact.class))
|
||||||
|
.body();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to retrieve available plugins from Kestra API. Cause: ", e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record PluginManifest(
|
public record PluginManifest(
|
||||||
String title,
|
String title,
|
||||||
String icon,
|
String icon,
|
||||||
@@ -153,4 +226,11 @@ public class PluginCatalogService {
|
|||||||
return groupId + ":" + artifactId + ":LATEST";
|
return groupId + ":" + artifactId + ":LATEST";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record ApiPluginArtifact(
|
||||||
|
String groupId,
|
||||||
|
String artifactId,
|
||||||
|
String license,
|
||||||
|
List<String> versions
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public class PluginClassLoader extends URLClassLoader {
|
|||||||
+ "|dev.failsafe"
|
+ "|dev.failsafe"
|
||||||
+ "|reactor"
|
+ "|reactor"
|
||||||
+ "|io.opentelemetry"
|
+ "|io.opentelemetry"
|
||||||
|
+ "|io.netty"
|
||||||
+ ")\\..*$");
|
+ ")\\..*$");
|
||||||
|
|
||||||
private final ClassLoader parent;
|
private final ClassLoader parent;
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ public class PluginResolver {
|
|||||||
final List<URL> resources = resolveUrlsForPluginPath(path);
|
final List<URL> resources = resolveUrlsForPluginPath(path);
|
||||||
plugins.add(new ExternalPlugin(
|
plugins.add(new ExternalPlugin(
|
||||||
path.toUri().toURL(),
|
path.toUri().toURL(),
|
||||||
resources.toArray(new URL[0]),
|
resources.toArray(new URL[0])
|
||||||
computeJarCrc32(path)
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} catch (final InvalidPathException | MalformedURLException e) {
|
} catch (final InvalidPathException | MalformedURLException e) {
|
||||||
@@ -124,33 +123,5 @@ public class PluginResolver {
|
|||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute a CRC32 of the JAR File without reading the whole file
|
|
||||||
*
|
|
||||||
* @param location of the JAR File.
|
|
||||||
* @return the CRC32 of {@code -1} if the checksum can't be computed.
|
|
||||||
*/
|
|
||||||
private static long computeJarCrc32(final Path location) {
|
|
||||||
CRC32 crc = new CRC32();
|
|
||||||
try (JarFile jar = new JarFile(location.toFile(), false)) {
|
|
||||||
Enumeration<JarEntry> entries = jar.entries();
|
|
||||||
while (entries.hasMoreElements()) {
|
|
||||||
JarEntry entry = entries.nextElement();
|
|
||||||
crc.update(entry.getName().getBytes());
|
|
||||||
crc.update(longToBytes(entry.getSize()));
|
|
||||||
crc.update(longToBytes(entry.getCrc()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return crc.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] longToBytes(long x) {
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
|
|
||||||
buffer.putLong(x);
|
|
||||||
return buffer.array();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
|||||||
|
|
||||||
static String extractPluginRawIdentifier(final JsonNode node, final boolean isVersioningSupported) {
|
static String extractPluginRawIdentifier(final JsonNode node, final boolean isVersioningSupported) {
|
||||||
String type = Optional.ofNullable(node.get(TYPE)).map(JsonNode::textValue).orElse(null);
|
String type = Optional.ofNullable(node.get(TYPE)).map(JsonNode::textValue).orElse(null);
|
||||||
String version = Optional.ofNullable(node.get(VERSION)).map(JsonNode::textValue).orElse(null);
|
String version = Optional.ofNullable(node.get(VERSION)).map(JsonNode::asText).orElse(null);
|
||||||
|
|
||||||
if (type == null || type.isEmpty()) {
|
if (type == null || type.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public interface QueueFactoryInterface {
|
|||||||
String SUBFLOWEXECUTIONRESULT_NAMED = "subflowExecutionResultQueue";
|
String SUBFLOWEXECUTIONRESULT_NAMED = "subflowExecutionResultQueue";
|
||||||
String CLUSTER_EVENT_NAMED = "clusterEventQueue";
|
String CLUSTER_EVENT_NAMED = "clusterEventQueue";
|
||||||
String SUBFLOWEXECUTIONEND_NAMED = "subflowExecutionEndQueue";
|
String SUBFLOWEXECUTIONEND_NAMED = "subflowExecutionEndQueue";
|
||||||
String EXECUTION_RUNNING_NAMED = "executionRunningQueue";
|
String MULTIPLE_CONDITION_EVENT_NAMED = "multipleConditionEventQueue";
|
||||||
|
|
||||||
QueueInterface<Execution> execution();
|
QueueInterface<Execution> execution();
|
||||||
|
|
||||||
@@ -58,5 +58,5 @@ public interface QueueFactoryInterface {
|
|||||||
|
|
||||||
QueueInterface<SubflowExecutionEnd> subflowExecutionEnd();
|
QueueInterface<SubflowExecutionEnd> subflowExecutionEnd();
|
||||||
|
|
||||||
QueueInterface<ExecutionRunning> executionRunning();
|
QueueInterface<MultipleConditionEvent> multipleConditionEvent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
|
|||||||
|
|
||||||
Integer purge(Execution execution);
|
Integer purge(Execution execution);
|
||||||
|
|
||||||
|
Integer purge(List<Execution> executions);
|
||||||
|
|
||||||
List<DailyExecutionStatistics> dailyStatisticsForAllTenants(
|
List<DailyExecutionStatistics> dailyStatisticsForAllTenants(
|
||||||
@Nullable String query,
|
@Nullable String query,
|
||||||
@Nullable String namespace,
|
@Nullable String namespace,
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ public interface FlowRepositoryInterface {
|
|||||||
* Used only if result is used internally and not exposed to the user.
|
* Used only if result is used internally and not exposed to the user.
|
||||||
* It is useful when we want to restart/resume a flow.
|
* It is useful when we want to restart/resume a flow.
|
||||||
*/
|
*/
|
||||||
default Flow findByExecutionWithoutAcl(Execution execution) {
|
default FlowWithSource findByExecutionWithoutAcl(Execution execution) {
|
||||||
Optional<Flow> find = this.findByIdWithoutAcl(
|
Optional<FlowWithSource> find = this.findByIdWithSourceWithoutAcl(
|
||||||
execution.getTenantId(),
|
execution.getTenantId(),
|
||||||
execution.getNamespace(),
|
execution.getNamespace(),
|
||||||
execution.getFlowId(),
|
execution.getFlowId(),
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ public interface LogRepositoryInterface extends SaveRepositoryInterface<LogEntry
|
|||||||
|
|
||||||
Integer purge(Execution execution);
|
Integer purge(Execution execution);
|
||||||
|
|
||||||
|
Integer purge(List<Execution> executions);
|
||||||
|
|
||||||
void deleteByQuery(String tenantId, String executionId, String taskId, String taskRunId, Level minLevel, Integer attempt);
|
void deleteByQuery(String tenantId, String executionId, String taskId, String taskRunId, Level minLevel, Integer attempt);
|
||||||
|
|
||||||
void deleteByQuery(String tenantId, String namespace, String flowId, String triggerId);
|
void deleteByQuery(String tenantId, String namespace, String flowId, String triggerId);
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public interface MetricRepositoryInterface extends SaveRepositoryInterface<Metri
|
|||||||
|
|
||||||
Integer purge(Execution execution);
|
Integer purge(Execution execution);
|
||||||
|
|
||||||
|
Integer purge(List<Execution> executions);
|
||||||
|
|
||||||
Flux<MetricEntry> findAllAsync(@Nullable String tenantId);
|
Flux<MetricEntry> findAllAsync(@Nullable String tenantId);
|
||||||
|
|
||||||
default Function<String, String> sortMapping() throws IllegalArgumentException {
|
default Function<String, String> sortMapping() throws IllegalArgumentException {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.kestra.core.runners;
|
||||||
|
|
||||||
|
import io.kestra.core.models.HasUID;
|
||||||
|
import io.kestra.core.utils.IdUtils;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Value;
|
||||||
|
import lombok.With;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ConcurrencyLimit implements HasUID {
|
||||||
|
@NotNull
|
||||||
|
String tenantId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
String namespace;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
String flowId;
|
||||||
|
|
||||||
|
@With
|
||||||
|
Integer running;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String uid() {
|
||||||
|
return IdUtils.fromPartsAndSeparator('|', this.tenantId, this.namespace, this.flowId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,5 +32,7 @@ public class ExecutionRunning implements HasUID {
|
|||||||
return IdUtils.fromPartsAndSeparator('|', this.tenantId, this.namespace, this.flowId, this.execution.getId());
|
return IdUtils.fromPartsAndSeparator('|', this.tenantId, this.namespace, this.flowId, this.execution.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ConcurrencyState { CREATED, RUNNING, QUEUED, CANCELLED, FAILED }
|
// Note: the KILLED state is only used in the Kafka implementation to difference between purging terminated running execution (null)
|
||||||
|
// and purging killed execution which need special treatment
|
||||||
|
public enum ConcurrencyState { CREATED, RUNNING, QUEUED, CANCELLED, FAILED, KILLED }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ public abstract class FilesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String resolveUniqueNameForFile(final Path path) {
|
private static String resolveUniqueNameForFile(final Path path) {
|
||||||
return IdUtils.from(path.toString()) + "-" + path.toFile().getName();
|
String filename = path.getFileName().toString().replace(' ', '+');
|
||||||
|
return IdUtils.from(path.toString()) + "-" + filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.kestra.core.runners;
|
package io.kestra.core.runners;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import io.kestra.core.encryption.EncryptionService;
|
import io.kestra.core.encryption.EncryptionService;
|
||||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||||
import io.kestra.core.exceptions.KestraRuntimeException;
|
import io.kestra.core.exceptions.KestraRuntimeException;
|
||||||
@@ -73,31 +72,28 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
|
|||||||
public class FlowInputOutput {
|
public class FlowInputOutput {
|
||||||
private static final Pattern URI_PATTERN = Pattern.compile("^[a-z]+:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$");
|
private static final Pattern URI_PATTERN = Pattern.compile("^[a-z]+:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$");
|
||||||
private static final ObjectMapper YAML_MAPPER = JacksonMapper.ofYaml();
|
private static final ObjectMapper YAML_MAPPER = JacksonMapper.ofYaml();
|
||||||
|
|
||||||
private final StorageInterface storageInterface;
|
private final StorageInterface storageInterface;
|
||||||
private final Optional<String> secretKey;
|
private final Optional<String> secretKey;
|
||||||
private final RunContextFactory runContextFactory;
|
private final RunContextFactory runContextFactory;
|
||||||
private final VariableRenderer variableRenderer;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FlowInputOutput(
|
public FlowInputOutput(
|
||||||
StorageInterface storageInterface,
|
StorageInterface storageInterface,
|
||||||
RunContextFactory runContextFactory,
|
RunContextFactory runContextFactory,
|
||||||
VariableRenderer variableRenderer,
|
|
||||||
@Nullable @Value("${kestra.encryption.secret-key}") String secretKey
|
@Nullable @Value("${kestra.encryption.secret-key}") String secretKey
|
||||||
) {
|
) {
|
||||||
this.storageInterface = storageInterface;
|
this.storageInterface = storageInterface;
|
||||||
this.runContextFactory = runContextFactory;
|
this.runContextFactory = runContextFactory;
|
||||||
this.secretKey = Optional.ofNullable(secretKey);
|
this.secretKey = Optional.ofNullable(secretKey);
|
||||||
this.variableRenderer = variableRenderer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate all the inputs of a given execution of a flow.
|
* Validate all the inputs of a given execution of a flow.
|
||||||
*
|
*
|
||||||
* @param inputs The Flow's inputs.
|
* @param inputs The Flow's inputs.
|
||||||
* @param execution The Execution.
|
* @param execution The Execution.
|
||||||
* @param data The Execution's inputs data.
|
* @param data The Execution's inputs data.
|
||||||
* @return The list of {@link InputAndValue}.
|
* @return The list of {@link InputAndValue}.
|
||||||
*/
|
*/
|
||||||
public Mono<List<InputAndValue>> validateExecutionInputs(final List<Input<?>> inputs,
|
public Mono<List<InputAndValue>> validateExecutionInputs(final List<Input<?>> inputs,
|
||||||
@@ -105,10 +101,11 @@ public class FlowInputOutput {
|
|||||||
final Execution execution,
|
final Execution execution,
|
||||||
final Publisher<CompletedPart> data) {
|
final Publisher<CompletedPart> data) {
|
||||||
if (ListUtils.isEmpty(inputs)) return Mono.just(Collections.emptyList());
|
if (ListUtils.isEmpty(inputs)) return Mono.just(Collections.emptyList());
|
||||||
|
|
||||||
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, flow, execution, inputData));
|
return readData(inputs, execution, data, false)
|
||||||
|
.map(inputData -> resolveInputs(inputs, flow, execution, inputData, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads all the inputs of a given execution of a flow.
|
* Reads all the inputs of a given execution of a flow.
|
||||||
*
|
*
|
||||||
@@ -122,7 +119,7 @@ public class FlowInputOutput {
|
|||||||
final Publisher<CompletedPart> data) {
|
final Publisher<CompletedPart> data) {
|
||||||
return this.readExecutionInputs(flow.getInputs(), flow, execution, data);
|
return this.readExecutionInputs(flow.getInputs(), flow, execution, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads all the inputs of a given execution of a flow.
|
* Reads all the inputs of a given execution of a flow.
|
||||||
*
|
*
|
||||||
@@ -137,7 +134,7 @@ public class FlowInputOutput {
|
|||||||
final Publisher<CompletedPart> data) {
|
final Publisher<CompletedPart> data) {
|
||||||
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, flow, execution, inputData));
|
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, flow, execution, inputData));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
|
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
|
||||||
return Flux.from(data)
|
return Flux.from(data)
|
||||||
.publishOn(Schedulers.boundedElastic())
|
.publishOn(Schedulers.boundedElastic())
|
||||||
@@ -220,7 +217,7 @@ public class FlowInputOutput {
|
|||||||
final Execution execution,
|
final Execution execution,
|
||||||
final Map<String, ?> data
|
final Map<String, ?> data
|
||||||
) {
|
) {
|
||||||
Map<String, Object> resolved = this.resolveInputs(inputs, flow, execution, data)
|
Map<String, Object> resolved = this.resolveInputs(inputs, flow, execution, data, true)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(InputAndValue::enabled)
|
.filter(InputAndValue::enabled)
|
||||||
.map(it -> {
|
.map(it -> {
|
||||||
@@ -233,7 +230,7 @@ public class FlowInputOutput {
|
|||||||
.collect(HashMap::new, (m,v)-> m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
.collect(HashMap::new, (m,v)-> m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
||||||
return MapUtils.flattenToNestedMap(resolved);
|
return MapUtils.flattenToNestedMap(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method for retrieving types inputs.
|
* Utility method for retrieving types inputs.
|
||||||
*
|
*
|
||||||
@@ -242,12 +239,21 @@ public class FlowInputOutput {
|
|||||||
* @param data The Execution's inputs data.
|
* @param data The Execution's inputs data.
|
||||||
* @return The Map of typed inputs.
|
* @return The Map of typed inputs.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
|
||||||
public List<InputAndValue> resolveInputs(
|
public List<InputAndValue> resolveInputs(
|
||||||
final List<Input<?>> inputs,
|
final List<Input<?>> inputs,
|
||||||
final FlowInterface flow,
|
final FlowInterface flow,
|
||||||
final Execution execution,
|
final Execution execution,
|
||||||
final Map<String, ?> data
|
final Map<String, ?> data
|
||||||
|
) {
|
||||||
|
return resolveInputs(inputs, flow, execution, data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InputAndValue> resolveInputs(
|
||||||
|
final List<Input<?>> inputs,
|
||||||
|
final FlowInterface flow,
|
||||||
|
final Execution execution,
|
||||||
|
final Map<String, ?> data,
|
||||||
|
final boolean decryptSecrets
|
||||||
) {
|
) {
|
||||||
if (inputs == null) {
|
if (inputs == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@@ -257,7 +263,7 @@ public class FlowInputOutput {
|
|||||||
.map(input -> ResolvableInput.of(input,data.get(input.getId())))
|
.map(input -> ResolvableInput.of(input,data.get(input.getId())))
|
||||||
.collect(Collectors.toMap(it -> it.get().input().getId(), Function.identity(), (o1, o2) -> o1, LinkedHashMap::new)));
|
.collect(Collectors.toMap(it -> it.get().input().getId(), Function.identity(), (o1, o2) -> o1, LinkedHashMap::new)));
|
||||||
|
|
||||||
resolvableInputMap.values().forEach(input -> resolveInputValue(input, flow, execution, resolvableInputMap));
|
resolvableInputMap.values().forEach(input -> resolveInputValue(input, flow, execution, resolvableInputMap, decryptSecrets));
|
||||||
|
|
||||||
return resolvableInputMap.values().stream().map(ResolvableInput::get).toList();
|
return resolvableInputMap.values().stream().map(ResolvableInput::get).toList();
|
||||||
}
|
}
|
||||||
@@ -267,7 +273,8 @@ public class FlowInputOutput {
|
|||||||
final @NotNull ResolvableInput resolvable,
|
final @NotNull ResolvableInput resolvable,
|
||||||
final FlowInterface flow,
|
final FlowInterface flow,
|
||||||
final @NotNull Execution execution,
|
final @NotNull Execution execution,
|
||||||
final @NotNull Map<String, ResolvableInput> inputs) {
|
final @NotNull Map<String, ResolvableInput> inputs,
|
||||||
|
final boolean decryptSecrets) {
|
||||||
|
|
||||||
// return immediately if the input is already resolved
|
// return immediately if the input is already resolved
|
||||||
if (resolvable.isResolved()) return resolvable.get();
|
if (resolvable.isResolved()) return resolvable.get();
|
||||||
@@ -275,9 +282,10 @@ public class FlowInputOutput {
|
|||||||
Input<?> input = resolvable.get().input();
|
Input<?> input = resolvable.get().input();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// resolve all input dependencies and check whether input is enabled
|
// Resolve all input dependencies and check whether input is enabled
|
||||||
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, flow, execution, inputs);
|
// Note: Secrets are always decrypted here because they can be part of expressions used to render inputs such as SELECT & MULTI_SELECT.
|
||||||
final RunContext runContext = buildRunContextForExecutionAndInputs(flow, execution, dependencies);
|
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, flow, execution, inputs, true);
|
||||||
|
final RunContext runContext = buildRunContextForExecutionAndInputs(flow, execution, dependencies, true);
|
||||||
|
|
||||||
boolean isInputEnabled = dependencies.isEmpty() || dependencies.values().stream().allMatch(InputAndValue::enabled);
|
boolean isInputEnabled = dependencies.isEmpty() || dependencies.values().stream().allMatch(InputAndValue::enabled);
|
||||||
|
|
||||||
@@ -312,13 +320,13 @@ public class FlowInputOutput {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
resolvable.setInput(input);
|
resolvable.setInput(input);
|
||||||
|
|
||||||
|
|
||||||
Object value = resolvable.get().value();
|
Object value = resolvable.get().value();
|
||||||
|
|
||||||
// resolve default if needed
|
// resolve default if needed
|
||||||
if (value == null && input.getDefaults() != null) {
|
if (value == null && input.getDefaults() != null) {
|
||||||
value = resolveDefaultValue(input, runContext);
|
RunContext runContextForDefault = decryptSecrets ? runContext : buildRunContextForExecutionAndInputs(flow, execution, dependencies, false);
|
||||||
|
value = resolveDefaultValue(input, runContextForDefault);
|
||||||
resolvable.isDefault(true);
|
resolvable.isDefault(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,8 +384,8 @@ public class FlowInputOutput {
|
|||||||
private static <T> Object resolveDefaultPropertyAsList(Input<?> input, PropertyContext renderer, Class<T> clazz) throws IllegalVariableEvaluationException {
|
private static <T> Object resolveDefaultPropertyAsList(Input<?> input, PropertyContext renderer, Class<T> clazz) throws IllegalVariableEvaluationException {
|
||||||
return Property.asList((Property<List<T>>) input.getDefaults(), renderer, clazz);
|
return Property.asList((Property<List<T>>) input.getDefaults(), renderer, clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RunContext buildRunContextForExecutionAndInputs(final FlowInterface flow, final Execution execution, Map<String, InputAndValue> dependencies) {
|
private RunContext buildRunContextForExecutionAndInputs(final FlowInterface flow, final Execution execution, Map<String, InputAndValue> dependencies, final boolean decryptSecrets) {
|
||||||
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
|
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue().value()), HashMap::putAll)
|
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue().value()), HashMap::putAll)
|
||||||
@@ -391,10 +399,10 @@ public class FlowInputOutput {
|
|||||||
flattenInputs.put(input.getId(), null);
|
flattenInputs.put(input.getId(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return runContextFactory.of(flow, execution, vars -> vars.withInputs(flattenInputs));
|
return runContextFactory.of(flow, execution, vars -> vars.withInputs(flattenInputs), decryptSecrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final FlowInterface 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, final boolean decryptSecrets) {
|
||||||
return Optional.ofNullable(input.getDependsOn())
|
return Optional.ofNullable(input.getDependsOn())
|
||||||
.map(DependsOn::inputs)
|
.map(DependsOn::inputs)
|
||||||
.stream()
|
.stream()
|
||||||
@@ -402,7 +410,7 @@ public class FlowInputOutput {
|
|||||||
.filter(id -> !id.equals(input.getId()))
|
.filter(id -> !id.equals(input.getId()))
|
||||||
.map(inputs::get)
|
.map(inputs::get)
|
||||||
.filter(Objects::nonNull) // input may declare unknown or non-necessary dependencies. Let's ignore.
|
.filter(Objects::nonNull) // input may declare unknown or non-necessary dependencies. Let's ignore.
|
||||||
.map(it -> resolveInputValue(it, flow, execution, inputs))
|
.map(it -> resolveInputValue(it, flow, execution, inputs, decryptSecrets))
|
||||||
.collect(Collectors.toMap(it -> it.input().getId(), Function.identity()));
|
.collect(Collectors.toMap(it -> it.input().getId(), Function.identity()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ public class FlowableUtils {
|
|||||||
|
|
||||||
ArrayList<ResolvedTask> result = new ArrayList<>();
|
ArrayList<ResolvedTask> result = new ArrayList<>();
|
||||||
|
|
||||||
int index = 0;
|
int iteration = 0;
|
||||||
for (Object current : distinctValue) {
|
for (Object current : distinctValue) {
|
||||||
try {
|
try {
|
||||||
String resolvedValue = current instanceof String stringValue ? stringValue : MAPPER.writeValueAsString(current);
|
String resolvedValue = current instanceof String stringValue ? stringValue : MAPPER.writeValueAsString(current);
|
||||||
@@ -508,7 +508,7 @@ public class FlowableUtils {
|
|||||||
result.add(ResolvedTask.builder()
|
result.add(ResolvedTask.builder()
|
||||||
.task(task)
|
.task(task)
|
||||||
.value(resolvedValue)
|
.value(resolvedValue)
|
||||||
.iteration(index++)
|
.iteration(iteration)
|
||||||
.parentId(parentTaskRun.getId())
|
.parentId(parentTaskRun.getId())
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
@@ -516,6 +516,7 @@ public class FlowableUtils {
|
|||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalVariableEvaluationException(e);
|
throw new IllegalVariableEvaluationException(e);
|
||||||
}
|
}
|
||||||
|
iteration++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package io.kestra.core.runners;
|
||||||
|
|
||||||
|
import io.kestra.core.models.HasUID;
|
||||||
|
import io.kestra.core.models.executions.Execution;
|
||||||
|
import io.kestra.core.models.flows.Flow;
|
||||||
|
import io.kestra.core.utils.IdUtils;
|
||||||
|
|
||||||
|
public record MultipleConditionEvent(Flow flow, Execution execution) implements HasUID {
|
||||||
|
@Override
|
||||||
|
public String uid() {
|
||||||
|
return IdUtils.fromParts(flow.uidWithoutRevision(), execution.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,9 @@ public class RunContextFactory {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected VariableRenderer variableRenderer;
|
protected VariableRenderer variableRenderer;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected SecureVariableRendererFactory secureVariableRendererFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected StorageInterface storageInterface;
|
protected StorageInterface storageInterface;
|
||||||
@@ -82,22 +85,33 @@ public class RunContextFactory {
|
|||||||
public RunContext of(FlowInterface flow, Execution execution) {
|
public RunContext of(FlowInterface flow, Execution execution) {
|
||||||
return of(flow, execution, Function.identity());
|
return of(flow, execution, Function.identity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RunContext of(FlowInterface flow, Execution execution, boolean decryptVariable) {
|
||||||
|
return of(flow, execution, Function.identity(), decryptVariable);
|
||||||
|
}
|
||||||
|
|
||||||
public RunContext of(FlowInterface flow, Execution execution, Function<RunVariables.Builder, RunVariables.Builder> runVariableModifier) {
|
public RunContext of(FlowInterface flow, Execution execution, Function<RunVariables.Builder, RunVariables.Builder> runVariableModifier) {
|
||||||
|
return of(flow, execution, runVariableModifier, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RunContext of(FlowInterface flow, Execution execution, Function<RunVariables.Builder, RunVariables.Builder> runVariableModifier, boolean decryptVariables) {
|
||||||
RunContextLogger runContextLogger = runContextLoggerFactory.create(execution);
|
RunContextLogger runContextLogger = runContextLoggerFactory.create(execution);
|
||||||
|
|
||||||
|
VariableRenderer variableRenderer = decryptVariables ? this.variableRenderer : secureVariableRendererFactory.createOrGet();
|
||||||
|
|
||||||
return newBuilder()
|
return newBuilder()
|
||||||
// Logger
|
// Logger
|
||||||
.withLogger(runContextLogger)
|
.withLogger(runContextLogger)
|
||||||
// Execution
|
// Execution
|
||||||
.withPluginConfiguration(Map.of())
|
.withPluginConfiguration(Map.of())
|
||||||
.withStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forExecution(execution), storageInterface, flowService))
|
.withStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forExecution(execution), storageInterface, flowService))
|
||||||
|
.withVariableRenderer(variableRenderer)
|
||||||
.withVariables(runVariableModifier.apply(
|
.withVariables(runVariableModifier.apply(
|
||||||
newRunVariablesBuilder()
|
newRunVariablesBuilder()
|
||||||
.withFlow(flow)
|
.withFlow(flow)
|
||||||
.withExecution(execution)
|
.withExecution(execution)
|
||||||
.withDecryptVariables(true)
|
.withDecryptVariables(decryptVariables)
|
||||||
.withSecretInputs(secretInputsFromFlow(flow))
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
)
|
)
|
||||||
.build(runContextLogger, PropertyContext.create(variableRenderer)))
|
.build(runContextLogger, PropertyContext.create(variableRenderer)))
|
||||||
.withSecretInputs(secretInputsFromFlow(flow))
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
@@ -109,7 +123,7 @@ public class RunContextFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RunContext of(FlowInterface flow, Task task, Execution execution, TaskRun taskRun, boolean decryptVariables) {
|
public RunContext of(FlowInterface flow, Task task, Execution execution, TaskRun taskRun, boolean decryptVariables) {
|
||||||
return this.of(flow, task, execution, taskRun, decryptVariables, variableRenderer);
|
return this.of(flow, task, execution, taskRun, decryptVariables, this.variableRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RunContext of(FlowInterface flow, Task task, Execution execution, TaskRun taskRun, boolean decryptVariables, VariableRenderer variableRenderer) {
|
public RunContext of(FlowInterface flow, Task task, Execution execution, TaskRun taskRun, boolean decryptVariables, VariableRenderer variableRenderer) {
|
||||||
@@ -147,7 +161,7 @@ public class RunContextFactory {
|
|||||||
.withFlow(flow)
|
.withFlow(flow)
|
||||||
.withTrigger(trigger)
|
.withTrigger(trigger)
|
||||||
.withSecretInputs(secretInputsFromFlow(flow))
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build(runContextLogger, PropertyContext.create(variableRenderer))
|
.build(runContextLogger, PropertyContext.create(this.variableRenderer))
|
||||||
)
|
)
|
||||||
.withSecretInputs(secretInputsFromFlow(flow))
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.withTrigger(trigger)
|
.withTrigger(trigger)
|
||||||
@@ -226,7 +240,7 @@ public class RunContextFactory {
|
|||||||
// inject mandatory services and config
|
// inject mandatory services and config
|
||||||
.withApplicationContext(applicationContext) // TODO - ideally application should not be injected here
|
.withApplicationContext(applicationContext) // TODO - ideally application should not be injected here
|
||||||
.withMeterRegistry(metricRegistry)
|
.withMeterRegistry(metricRegistry)
|
||||||
.withVariableRenderer(variableRenderer)
|
.withVariableRenderer(this.variableRenderer)
|
||||||
.withStorageInterface(storageInterface)
|
.withStorageInterface(storageInterface)
|
||||||
.withSecretKey(secretKey)
|
.withSecretKey(secretKey)
|
||||||
.withWorkingDir(workingDirFactory.createWorkingDirectory())
|
.withWorkingDir(workingDirFactory.createWorkingDirectory())
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public final class RunVariables {
|
|||||||
* @return a new immutable {@link Map}.
|
* @return a new immutable {@link Map}.
|
||||||
*/
|
*/
|
||||||
static Map<String, Object> of(final AbstractTrigger trigger) {
|
static Map<String, Object> of(final AbstractTrigger trigger) {
|
||||||
return ImmutableMap.of(
|
return Map.of(
|
||||||
"id", trigger.getId(),
|
"id", trigger.getId(),
|
||||||
"type", trigger.getType()
|
"type", trigger.getType()
|
||||||
);
|
);
|
||||||
@@ -281,16 +281,19 @@ public final class RunVariables {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (flow != null && flow.getInputs() != null) {
|
if (flow != null && flow.getInputs() != null) {
|
||||||
|
// Create a new PropertyContext with 'flow' variables which are required by some pebble expressions.
|
||||||
|
PropertyContextWithVariables context = new PropertyContextWithVariables(propertyContext, Map.of("flow", RunVariables.of(flow)));
|
||||||
|
|
||||||
// we add default inputs value from the flow if not already set, this will be useful for triggers
|
// we add default inputs value from the flow if not already set, this will be useful for triggers
|
||||||
flow.getInputs().stream()
|
flow.getInputs().stream()
|
||||||
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
|
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
|
||||||
.forEach(input -> {
|
.forEach(input -> {
|
||||||
try {
|
try {
|
||||||
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, propertyContext));
|
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, context));
|
||||||
} catch (IllegalVariableEvaluationException e) {
|
} catch (IllegalVariableEvaluationException e) {
|
||||||
throw new RuntimeException("Unable to inject default value for input '" + input.getId() + "'", e);
|
// Silent catch, if an input depends on another input, or a variable that is populated at runtime / input filling time, we can't resolve it here.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputs.isEmpty()) {
|
if (!inputs.isEmpty()) {
|
||||||
@@ -390,4 +393,20 @@ public final class RunVariables {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private RunVariables(){}
|
private RunVariables(){}
|
||||||
|
|
||||||
|
private record PropertyContextWithVariables(
|
||||||
|
PropertyContext delegate,
|
||||||
|
Map<String, Object> variables
|
||||||
|
) implements PropertyContext {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
|
return delegate.render(inline, variables.isEmpty() ? this.variables : variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> render(Map<String, Object> inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
|
return delegate.render(inline, variables.isEmpty() ? this.variables : variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final class Secret {
|
|||||||
for (var entry: data.entrySet()) {
|
for (var entry: data.entrySet()) {
|
||||||
if (entry.getValue() instanceof Map map) {
|
if (entry.getValue() instanceof Map map) {
|
||||||
// if some value are of type EncryptedString we decode them and replace the object
|
// if some value are of type EncryptedString we decode them and replace the object
|
||||||
if (EncryptedString.TYPE.equalsIgnoreCase((String)map.get("type"))) {
|
if (map.get("type") instanceof String typeStr && EncryptedString.TYPE.equalsIgnoreCase(typeStr)) {
|
||||||
try {
|
try {
|
||||||
String decoded = decrypt((String) map.get("value"));
|
String decoded = decrypt((String) map.get("value"));
|
||||||
decryptedMap.put(entry.getKey(), decoded);
|
decryptedMap.put(entry.getKey(), decoded);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package io.kestra.core.runners;
|
||||||
|
|
||||||
|
import io.kestra.core.runners.pebble.PebbleEngineFactory;
|
||||||
|
import io.kestra.core.runners.pebble.functions.SecretFunction;
|
||||||
|
import io.micronaut.context.ApplicationContext;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class SecureVariableRendererFactory {
|
||||||
|
|
||||||
|
private final PebbleEngineFactory pebbleEngineFactory;
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private VariableRenderer secureVariableRenderer;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SecureVariableRendererFactory(ApplicationContext applicationContext, PebbleEngineFactory pebbleEngineFactory) {
|
||||||
|
this.pebbleEngineFactory = pebbleEngineFactory;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or returns the existing secured {@link VariableRenderer} instance.
|
||||||
|
*
|
||||||
|
* @return the secured {@link VariableRenderer} instance
|
||||||
|
*/
|
||||||
|
public synchronized VariableRenderer createOrGet() {
|
||||||
|
if (this.secureVariableRenderer == null) {
|
||||||
|
// Explicitly create a new instance through the application context to ensure
|
||||||
|
// eventual custom VariableRenderer implementation is used
|
||||||
|
secureVariableRenderer = applicationContext.createBean(VariableRenderer.class);
|
||||||
|
secureVariableRenderer.setPebbleEngine(pebbleEngineFactory.createWithMaskedFunctions(secureVariableRenderer, List.of(SecretFunction.NAME)));
|
||||||
|
}
|
||||||
|
return secureVariableRenderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,121 +2,44 @@ package io.kestra.core.runners;
|
|||||||
|
|
||||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||||
import io.kestra.core.runners.pebble.*;
|
import io.kestra.core.runners.pebble.*;
|
||||||
import io.kestra.core.runners.pebble.functions.RenderingFunctionInterface;
|
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import io.micronaut.context.annotation.ConfigurationProperties;
|
import io.micronaut.context.annotation.ConfigurationProperties;
|
||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import io.pebbletemplates.pebble.PebbleEngine;
|
import io.pebbletemplates.pebble.PebbleEngine;
|
||||||
import io.pebbletemplates.pebble.error.AttributeNotFoundException;
|
import io.pebbletemplates.pebble.error.AttributeNotFoundException;
|
||||||
import io.pebbletemplates.pebble.error.PebbleException;
|
import io.pebbletemplates.pebble.error.PebbleException;
|
||||||
import io.pebbletemplates.pebble.extension.Extension;
|
|
||||||
import io.pebbletemplates.pebble.extension.Function;
|
|
||||||
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class VariableRenderer {
|
public class VariableRenderer {
|
||||||
private static final Pattern RAW_PATTERN = Pattern.compile("(\\{%-*\\s*raw\\s*-*%}(.*?)\\{%-*\\s*endraw\\s*-*%})");
|
private static final Pattern RAW_PATTERN = Pattern.compile("(\\{%-*\\s*raw\\s*-*%}(.*?)\\{%-*\\s*endraw\\s*-*%})");
|
||||||
public static final int MAX_RENDERING_AMOUNT = 100;
|
public static final int MAX_RENDERING_AMOUNT = 100;
|
||||||
|
|
||||||
private final PebbleEngine pebbleEngine;
|
private PebbleEngine pebbleEngine;
|
||||||
private final VariableConfiguration variableConfiguration;
|
private final VariableConfiguration variableConfiguration;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public VariableRenderer(ApplicationContext applicationContext, @Nullable VariableConfiguration variableConfiguration) {
|
public VariableRenderer(ApplicationContext applicationContext, @Nullable VariableConfiguration variableConfiguration) {
|
||||||
this(applicationContext, variableConfiguration, Collections.emptyList());
|
this(applicationContext.getBean(PebbleEngineFactory.class), variableConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public VariableRenderer(ApplicationContext applicationContext, @Nullable VariableConfiguration variableConfiguration, List<String> functionsToMask) {
|
public VariableRenderer(PebbleEngineFactory pebbleEngineFactory, @Nullable VariableConfiguration variableConfiguration) {
|
||||||
this.variableConfiguration = variableConfiguration != null ? variableConfiguration : new VariableConfiguration();
|
this.variableConfiguration = variableConfiguration != null ? variableConfiguration : new VariableConfiguration();
|
||||||
|
this.pebbleEngine = pebbleEngineFactory.create();
|
||||||
PebbleEngine.Builder pebbleBuilder = new PebbleEngine.Builder()
|
|
||||||
.registerExtensionCustomizer(ExtensionCustomizer::new)
|
|
||||||
.strictVariables(true)
|
|
||||||
.cacheActive(this.variableConfiguration.getCacheEnabled())
|
|
||||||
.newLineTrimming(false)
|
|
||||||
.autoEscaping(false);
|
|
||||||
|
|
||||||
List<Extension> extensions = applicationContext.getBeansOfType(Extension.class).stream()
|
|
||||||
.map(e -> functionsToMask.stream().anyMatch(excludedFunction -> e.getFunctions().containsKey(excludedFunction))
|
|
||||||
? extensionWithMaskedFunctions(e, functionsToMask)
|
|
||||||
: e)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
extensions.forEach(pebbleBuilder::extension);
|
|
||||||
|
|
||||||
if (this.variableConfiguration.getCacheEnabled()) {
|
|
||||||
pebbleBuilder.templateCache(new PebbleLruCache(this.variableConfiguration.getCacheSize()));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pebbleEngine = pebbleBuilder.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Extension extensionWithMaskedFunctions(Extension initialExtension, List<String> maskedFunctions) {
|
public void setPebbleEngine(final PebbleEngine pebbleEngine) {
|
||||||
return (Extension) Proxy.newProxyInstance(
|
this.pebbleEngine = pebbleEngine;
|
||||||
initialExtension.getClass().getClassLoader(),
|
|
||||||
new Class[]{Extension.class},
|
|
||||||
(proxy, method, methodArgs) -> {
|
|
||||||
if (method.getName().equals("getFunctions")) {
|
|
||||||
return initialExtension.getFunctions().entrySet().stream()
|
|
||||||
.map(entry -> {
|
|
||||||
if (maskedFunctions.contains(entry.getKey())) {
|
|
||||||
return Map.entry(entry.getKey(), this.maskedFunctionProxy(entry.getValue()));
|
|
||||||
} else if (RenderingFunctionInterface.class.isAssignableFrom(entry.getValue().getClass())) {
|
|
||||||
return Map.entry(entry.getKey(), this.variableRendererProxy(entry.getValue()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
return method.invoke(initialExtension, methodArgs);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function variableRendererProxy(Function initialFunction) {
|
|
||||||
return (Function) Proxy.newProxyInstance(
|
|
||||||
initialFunction.getClass().getClassLoader(),
|
|
||||||
new Class[]{Function.class, RenderingFunctionInterface.class},
|
|
||||||
(functionProxy, functionMethod, functionArgs) -> {
|
|
||||||
if (functionMethod.getName().equals("variableRenderer")) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
return functionMethod.invoke(initialFunction, functionArgs);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Function maskedFunctionProxy(Function initialFunction) {
|
|
||||||
return (Function) Proxy.newProxyInstance(
|
|
||||||
initialFunction.getClass().getClassLoader(),
|
|
||||||
new Class[]{Function.class},
|
|
||||||
(functionProxy, functionMethod, functionArgs) -> {
|
|
||||||
Object result;
|
|
||||||
try {
|
|
||||||
result = functionMethod.invoke(initialFunction, functionArgs);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
throw e.getCause();
|
|
||||||
}
|
|
||||||
if (functionMethod.getName().equals("execute")) {
|
|
||||||
return "******";
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IllegalVariableEvaluationException properPebbleException(PebbleException initialExtension) {
|
public static IllegalVariableEvaluationException properPebbleException(PebbleException initialExtension) {
|
||||||
if (initialExtension instanceof AttributeNotFoundException current) {
|
if (initialExtension instanceof AttributeNotFoundException current) {
|
||||||
return new IllegalVariableEvaluationException(
|
return new IllegalVariableEvaluationException(
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package io.kestra.core.runners.pebble;
|
||||||
|
|
||||||
|
import io.kestra.core.runners.VariableRenderer;
|
||||||
|
import io.kestra.core.runners.pebble.functions.RenderingFunctionInterface;
|
||||||
|
import io.micronaut.context.ApplicationContext;
|
||||||
|
import io.micronaut.core.annotation.Nullable;
|
||||||
|
import io.pebbletemplates.pebble.PebbleEngine;
|
||||||
|
import io.pebbletemplates.pebble.extension.Extension;
|
||||||
|
import io.pebbletemplates.pebble.extension.Function;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class PebbleEngineFactory {
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
private final VariableRenderer.VariableConfiguration variableConfiguration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PebbleEngineFactory(ApplicationContext applicationContext, @Nullable VariableRenderer.VariableConfiguration variableConfiguration) {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
this.variableConfiguration = variableConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PebbleEngine create() {
|
||||||
|
PebbleEngine.Builder builder = newPebbleEngineBuilder();
|
||||||
|
this.applicationContext.getBeansOfType(Extension.class).forEach(builder::extension);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PebbleEngine createWithMaskedFunctions(VariableRenderer renderer, final List<String> functionsToMask) {
|
||||||
|
|
||||||
|
PebbleEngine.Builder builder = newPebbleEngineBuilder();
|
||||||
|
|
||||||
|
this.applicationContext.getBeansOfType(Extension.class).stream()
|
||||||
|
.map(e -> functionsToMask.stream().anyMatch(fun -> e.getFunctions().containsKey(fun))
|
||||||
|
? extensionWithMaskedFunctions(renderer, e, functionsToMask)
|
||||||
|
: e)
|
||||||
|
.forEach(builder::extension);
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PebbleEngine.Builder newPebbleEngineBuilder() {
|
||||||
|
PebbleEngine.Builder builder = new PebbleEngine.Builder()
|
||||||
|
.registerExtensionCustomizer(ExtensionCustomizer::new)
|
||||||
|
.strictVariables(true)
|
||||||
|
.cacheActive(this.variableConfiguration.getCacheEnabled())
|
||||||
|
.newLineTrimming(false)
|
||||||
|
.autoEscaping(false);
|
||||||
|
|
||||||
|
if (this.variableConfiguration.getCacheEnabled()) {
|
||||||
|
builder = builder.templateCache(new PebbleLruCache(this.variableConfiguration.getCacheSize()));
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Extension extensionWithMaskedFunctions(VariableRenderer renderer, Extension initialExtension, List<String> maskedFunctions) {
|
||||||
|
return (Extension) Proxy.newProxyInstance(
|
||||||
|
initialExtension.getClass().getClassLoader(),
|
||||||
|
new Class[]{Extension.class},
|
||||||
|
(proxy, method, methodArgs) -> {
|
||||||
|
if (method.getName().equals("getFunctions")) {
|
||||||
|
return initialExtension.getFunctions().entrySet().stream()
|
||||||
|
.map(entry -> {
|
||||||
|
if (maskedFunctions.contains(entry.getKey())) {
|
||||||
|
return Map.entry(entry.getKey(), this.maskedFunctionProxy(entry.getValue()));
|
||||||
|
} else if (RenderingFunctionInterface.class.isAssignableFrom(entry.getValue().getClass())) {
|
||||||
|
return Map.entry(entry.getKey(), this.variableRendererProxy(renderer, entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return method.invoke(initialExtension, methodArgs);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function variableRendererProxy(VariableRenderer renderer, Function initialFunction) {
|
||||||
|
return (Function) Proxy.newProxyInstance(
|
||||||
|
initialFunction.getClass().getClassLoader(),
|
||||||
|
new Class[]{Function.class, RenderingFunctionInterface.class},
|
||||||
|
(functionProxy, functionMethod, functionArgs) -> {
|
||||||
|
if (functionMethod.getName().equals("variableRenderer")) {
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
|
return functionMethod.invoke(initialFunction, functionArgs);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function maskedFunctionProxy(Function initialFunction) {
|
||||||
|
return (Function) Proxy.newProxyInstance(
|
||||||
|
initialFunction.getClass().getClassLoader(),
|
||||||
|
new Class[]{Function.class},
|
||||||
|
(functionProxy, functionMethod, functionArgs) -> {
|
||||||
|
Object result;
|
||||||
|
try {
|
||||||
|
result = functionMethod.invoke(initialFunction, functionArgs);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw e.getCause();
|
||||||
|
}
|
||||||
|
if (functionMethod.getName().equals("execute")) {
|
||||||
|
return "******";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
package io.kestra.core.runners.pebble.filters;
|
package io.kestra.core.runners.pebble.filters;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import io.pebbletemplates.pebble.error.PebbleException;
|
import io.pebbletemplates.pebble.error.PebbleException;
|
||||||
import io.pebbletemplates.pebble.extension.Filter;
|
import io.pebbletemplates.pebble.extension.Filter;
|
||||||
import io.pebbletemplates.pebble.template.EvaluationContext;
|
import io.pebbletemplates.pebble.template.EvaluationContext;
|
||||||
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ChunkFilter implements Filter {
|
public class ChunkFilter implements Filter {
|
||||||
@Override
|
@Override
|
||||||
public List<String> getArgumentNames() {
|
public List<String> getArgumentNames() {
|
||||||
@@ -30,6 +31,10 @@ public class ChunkFilter implements Filter {
|
|||||||
throw new PebbleException(null, "'chunk' filter can only be applied to List. Actual type was: " + input.getClass().getName(), lineNumber, self.getName());
|
throw new PebbleException(null, "'chunk' filter can only be applied to List. Actual type was: " + input.getClass().getName(), lineNumber, self.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Lists.partition((List) input, ((Long) args.get("size")).intValue());
|
Object sizeObj = args.get("size");
|
||||||
|
if (!(sizeObj instanceof Number)) {
|
||||||
|
throw new PebbleException(null, "'chunk' filter argument 'size' must be a number. Actual type was: " + sizeObj.getClass().getName(), lineNumber, self.getName());
|
||||||
|
}
|
||||||
|
return Lists.partition((List) input, ((Number) sizeObj).intValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,17 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class JqFilter implements Filter {
|
public class JqFilter implements Filter {
|
||||||
private final Scope scope;
|
// Load Scope once as static to avoid repeated initialization
|
||||||
|
// This improves performance by loading builtin functions only once when the class loads
|
||||||
|
private static final Scope SCOPE;
|
||||||
private final List<String> argumentNames = new ArrayList<>();
|
private final List<String> argumentNames = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
SCOPE = Scope.newEmptyScope();
|
||||||
|
BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, SCOPE);
|
||||||
|
}
|
||||||
|
|
||||||
public JqFilter() {
|
public JqFilter() {
|
||||||
scope = Scope.newEmptyScope();
|
|
||||||
BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, scope);
|
|
||||||
this.argumentNames.add("expression");
|
this.argumentNames.add("expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +48,7 @@ public class JqFilter implements Filter {
|
|||||||
|
|
||||||
String pattern = (String) args.get("expression");
|
String pattern = (String) args.get("expression");
|
||||||
|
|
||||||
Scope rootScope = Scope.newEmptyScope();
|
|
||||||
BuiltinFunctionLoader.getInstance().loadFunctions(Versions.JQ_1_6, rootScope);
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
JsonQuery q = JsonQuery.compile(pattern, Versions.JQ_1_6);
|
JsonQuery q = JsonQuery.compile(pattern, Versions.JQ_1_6);
|
||||||
|
|
||||||
JsonNode in;
|
JsonNode in;
|
||||||
@@ -59,7 +61,7 @@ public class JqFilter implements Filter {
|
|||||||
final List<Object> out = new ArrayList<>();
|
final List<Object> out = new ArrayList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
q.apply(scope, in, v -> {
|
q.apply(Scope.newChildScope(SCOPE), in, v -> {
|
||||||
if (v instanceof TextNode) {
|
if (v instanceof TextNode) {
|
||||||
out.add(v.textValue());
|
out.add(v.textValue());
|
||||||
} else if (v instanceof NullNode) {
|
} else if (v instanceof NullNode) {
|
||||||
|
|||||||
@@ -151,10 +151,7 @@ abstract class AbstractFileFunction implements Function {
|
|||||||
// if there is a trigger of type execution, we also allow accessing a file from the parent execution
|
// if there is a trigger of type execution, we also allow accessing a file from the parent execution
|
||||||
Map<String, String> trigger = (Map<String, String>) context.getVariable(TRIGGER);
|
Map<String, String> trigger = (Map<String, String>) context.getVariable(TRIGGER);
|
||||||
|
|
||||||
if (!isFileUriValid(trigger.get(NAMESPACE), trigger.get("flowId"), trigger.get("executionId"), path)) {
|
return isFileUriValid(trigger.get(NAMESPACE), trigger.get("flowId"), trigger.get("executionId"), path);
|
||||||
throw new IllegalArgumentException("Unable to read the file '" + path + "' as it didn't belong to the parent execution");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class KvFunction implements Function {
|
|||||||
String key = getKey(args, self, lineNumber);
|
String key = getKey(args, self, lineNumber);
|
||||||
String namespace = (String) args.get(NAMESPACE_ARG);
|
String namespace = (String) args.get(NAMESPACE_ARG);
|
||||||
|
|
||||||
Boolean errorOnMissing = Optional.ofNullable((Boolean) args.get(ERROR_ON_MISSING_ARG)).orElse(true);
|
boolean errorOnMissing = Optional.ofNullable((Boolean) args.get(ERROR_ON_MISSING_ARG)).orElse(true);
|
||||||
|
|
||||||
Map<String, String> flow = (Map<String, String>) context.getVariable("flow");
|
Map<String, String> flow = (Map<String, String>) context.getVariable("flow");
|
||||||
String flowNamespace = flow.get(NAMESPACE_ARG);
|
String flowNamespace = flow.get(NAMESPACE_ARG);
|
||||||
@@ -53,11 +53,16 @@ public class KvFunction implements Function {
|
|||||||
// we didn't check allowedNamespace here as it's checked in the kvStoreService itself
|
// we didn't check allowedNamespace here as it's checked in the kvStoreService itself
|
||||||
value = kvStoreService.get(flowTenantId, namespace, flowNamespace).getValue(key);
|
value = kvStoreService.get(flowTenantId, namespace, flowNamespace).getValue(key);
|
||||||
}
|
}
|
||||||
|
} catch (ResourceExpiredException e) {
|
||||||
|
if (errorOnMissing) {
|
||||||
|
throw new PebbleException(e, e.getMessage(), lineNumber, self.getName());
|
||||||
|
}
|
||||||
|
value = Optional.empty();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new PebbleException(e, e.getMessage(), lineNumber, self.getName());
|
throw new PebbleException(e, e.getMessage(), lineNumber, self.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.isEmpty() && errorOnMissing == Boolean.TRUE) {
|
if (value.isEmpty() && errorOnMissing) {
|
||||||
throw new PebbleException(null, "The key '" + key + "' does not exist in the namespace '" + namespace + "'.", lineNumber, self.getName());
|
throw new PebbleException(null, "The key '" + key + "' does not exist in the namespace '" + namespace + "'.", lineNumber, self.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,4 +90,4 @@ public class KvFunction implements Function {
|
|||||||
|
|
||||||
return (String) args.get(KEY_ARGS);
|
return (String) args.get(KEY_ARGS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,31 +163,28 @@ public final class JacksonMapper {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Pair<JsonNode, JsonNode> getBiDirectionalDiffs(Object previous, Object current) {
|
public static Pair<JsonNode, JsonNode> getBiDirectionalDiffs(Object before, Object after) {
|
||||||
JsonNode previousJson = MAPPER.valueToTree(previous);
|
JsonNode beforeNode = MAPPER.valueToTree(before);
|
||||||
JsonNode newJson = MAPPER.valueToTree(current);
|
JsonNode afterNode = MAPPER.valueToTree(after);
|
||||||
|
|
||||||
JsonNode patchPrevToNew = JsonDiff.asJson(previousJson, newJson);
|
JsonNode patch = JsonDiff.asJson(beforeNode, afterNode);
|
||||||
JsonNode patchNewToPrev = JsonDiff.asJson(newJson, previousJson);
|
JsonNode revert = JsonDiff.asJson(afterNode, beforeNode);
|
||||||
|
|
||||||
return Pair.of(patchPrevToNew, patchNewToPrev);
|
return Pair.of(patch, revert);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String applyPatches(Object object, List<JsonNode> patches) throws JsonProcessingException {
|
public static JsonNode applyPatchesOnJsonNode(JsonNode jsonObject, List<JsonNode> patches) {
|
||||||
for (JsonNode patch : patches) {
|
for (JsonNode patch : patches) {
|
||||||
try {
|
try {
|
||||||
// Required for ES
|
// Required for ES
|
||||||
if (patch.findValue("value") == null) {
|
if (patch.findValue("value") == null && !patch.isEmpty()) {
|
||||||
((ObjectNode) patch.get(0)).set("value", (JsonNode) null);
|
((ObjectNode) patch.get(0)).set("value", null);
|
||||||
}
|
}
|
||||||
JsonNode current = MAPPER.valueToTree(object);
|
jsonObject = JsonPatch.fromJson(patch).apply(jsonObject);
|
||||||
object = JsonPatch.fromJson(patch).apply(current);
|
|
||||||
} catch (IOException | JsonPatchException e) {
|
} catch (IOException | JsonPatchException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MAPPER.writeValueAsString(object);
|
return jsonObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import io.kestra.core.models.flows.State;
|
|||||||
import io.kestra.core.models.flows.input.InputAndValue;
|
import io.kestra.core.models.flows.input.InputAndValue;
|
||||||
import io.kestra.core.models.hierarchies.AbstractGraphTask;
|
import io.kestra.core.models.hierarchies.AbstractGraphTask;
|
||||||
import io.kestra.core.models.hierarchies.GraphCluster;
|
import io.kestra.core.models.hierarchies.GraphCluster;
|
||||||
|
import io.kestra.core.models.tasks.FlowableTask;
|
||||||
import io.kestra.core.models.tasks.ResolvedTask;
|
import io.kestra.core.models.tasks.ResolvedTask;
|
||||||
import io.kestra.core.models.tasks.Task;
|
import io.kestra.core.models.tasks.Task;
|
||||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||||
@@ -56,8 +57,7 @@ import java.util.function.Predicate;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static io.kestra.core.utils.Rethrow.throwFunction;
|
import static io.kestra.core.utils.Rethrow.*;
|
||||||
import static io.kestra.core.utils.Rethrow.throwPredicate;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -122,21 +122,38 @@ public class ExecutionService {
|
|||||||
* Retry set the given taskRun in created state
|
* Retry set the given taskRun in created state
|
||||||
* and return the execution in running state
|
* and return the execution in running state
|
||||||
**/
|
**/
|
||||||
public Execution retryTask(Execution execution, String taskRunId) {
|
public Execution retryTask(Execution execution, Flow flow, String taskRunId) throws InternalException {
|
||||||
List<TaskRun> newTaskRuns = execution
|
TaskRun taskRun = execution.findTaskRunByTaskRunId(taskRunId).withState(State.Type.CREATED);
|
||||||
.getTaskRunList()
|
List<TaskRun> taskRunList = execution.getTaskRunList();
|
||||||
.stream()
|
|
||||||
.map(taskRun -> {
|
if (taskRun.getParentTaskRunId() != null) {
|
||||||
if (taskRun.getId().equals(taskRunId)) {
|
// we need to find the parent to remove any errors or finally tasks already executed
|
||||||
return taskRun
|
TaskRun parentTaskRun = execution.findTaskRunByTaskRunId(taskRun.getParentTaskRunId());
|
||||||
.withState(State.Type.CREATED);
|
Task parentTask = flow.findTaskByTaskId(parentTaskRun.getTaskId());
|
||||||
|
if (parentTask instanceof FlowableTask<?> flowableTask) {
|
||||||
|
if (flowableTask.getErrors() != null) {
|
||||||
|
List<Task> allErrors = Stream.concat(flowableTask.getErrors().stream()
|
||||||
|
.filter(task -> task.isFlowable() && ((FlowableTask<?>) task).getErrors() != null)
|
||||||
|
.flatMap(task -> ((FlowableTask<?>) task).getErrors().stream()),
|
||||||
|
flowableTask.getErrors().stream())
|
||||||
|
.toList();
|
||||||
|
allErrors.forEach(error -> taskRunList.removeIf(t -> t.getTaskId().equals(error.getId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
return taskRun;
|
if (flowableTask.getFinally() != null) {
|
||||||
})
|
List<Task> allFinally = Stream.concat(flowableTask.getFinally().stream()
|
||||||
.toList();
|
.filter(task -> task.isFlowable() && ((FlowableTask<?>) task).getFinally() != null)
|
||||||
|
.flatMap(task -> ((FlowableTask<?>) task).getFinally().stream()),
|
||||||
|
flowableTask.getFinally().stream())
|
||||||
|
.toList();
|
||||||
|
allFinally.forEach(error -> taskRunList.removeIf(t -> t.getTaskId().equals(error.getId())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return execution.withTaskRunList(newTaskRuns).withState(State.Type.RUNNING);
|
return execution.withTaskRunList(taskRunList).withTaskRun(taskRun).withState(State.Type.RUNNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
return execution.withTaskRun(taskRun).withState(State.Type.RUNNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Execution retryWaitFor(Execution execution, String flowableTaskRunId) {
|
public Execution retryWaitFor(Execution execution, String flowableTaskRunId) {
|
||||||
@@ -366,6 +383,7 @@ public class ExecutionService {
|
|||||||
if (!isFlowable || s.equals(taskRunId)) {
|
if (!isFlowable || s.equals(taskRunId)) {
|
||||||
TaskRun newTaskRun;
|
TaskRun newTaskRun;
|
||||||
|
|
||||||
|
State.Type targetState = newState;
|
||||||
if (task instanceof Pause pauseTask) {
|
if (task instanceof Pause pauseTask) {
|
||||||
State.Type terminalState = newState == State.Type.RUNNING ? State.Type.SUCCESS : newState;
|
State.Type terminalState = newState == State.Type.RUNNING ? State.Type.SUCCESS : newState;
|
||||||
Pause.Resumed _resumed = resumed != null ? resumed : Pause.Resumed.now(terminalState);
|
Pause.Resumed _resumed = resumed != null ? resumed : Pause.Resumed.now(terminalState);
|
||||||
@@ -375,23 +393,23 @@ public class ExecutionService {
|
|||||||
// if it's a Pause task with no subtask, we terminate the task
|
// if it's a Pause task with no subtask, we terminate the task
|
||||||
if (ListUtils.isEmpty(pauseTask.getTasks()) && ListUtils.isEmpty(pauseTask.getErrors()) && ListUtils.isEmpty(pauseTask.getFinally())) {
|
if (ListUtils.isEmpty(pauseTask.getTasks()) && ListUtils.isEmpty(pauseTask.getErrors()) && ListUtils.isEmpty(pauseTask.getFinally())) {
|
||||||
if (newState == State.Type.RUNNING) {
|
if (newState == State.Type.RUNNING) {
|
||||||
newTaskRun = newTaskRun.withState(State.Type.SUCCESS);
|
targetState = State.Type.SUCCESS;
|
||||||
} else if (newState == State.Type.KILLING) {
|
} else if (newState == State.Type.KILLING) {
|
||||||
newTaskRun = newTaskRun.withState(State.Type.KILLED);
|
targetState = State.Type.KILLED;
|
||||||
} else {
|
|
||||||
newTaskRun = newTaskRun.withState(newState);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// we should set the state to RUNNING so that subtasks are executed
|
// we should set the state to RUNNING so that subtasks are executed
|
||||||
newTaskRun = newTaskRun.withState(State.Type.RUNNING);
|
targetState = State.Type.RUNNING;
|
||||||
}
|
}
|
||||||
|
newTaskRun = newTaskRun.withState(targetState);
|
||||||
} else {
|
} else {
|
||||||
newTaskRun = originalTaskRun.withState(newState);
|
newTaskRun = originalTaskRun.withState(targetState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (originalTaskRun.getAttempts() != null && !originalTaskRun.getAttempts().isEmpty()) {
|
if (originalTaskRun.getAttempts() != null && !originalTaskRun.getAttempts().isEmpty()) {
|
||||||
ArrayList<TaskRunAttempt> attempts = new ArrayList<>(originalTaskRun.getAttempts());
|
ArrayList<TaskRunAttempt> attempts = new ArrayList<>(originalTaskRun.getAttempts());
|
||||||
attempts.set(attempts.size() - 1, attempts.getLast().withState(newState));
|
attempts.set(attempts.size() - 1, attempts.getLast().withState(targetState));
|
||||||
newTaskRun = newTaskRun.withAttempts(attempts);
|
newTaskRun = newTaskRun.withAttempts(attempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +449,8 @@ public class ExecutionService {
|
|||||||
@Nullable String flowId,
|
@Nullable String flowId,
|
||||||
@Nullable ZonedDateTime startDate,
|
@Nullable ZonedDateTime startDate,
|
||||||
@Nullable ZonedDateTime endDate,
|
@Nullable ZonedDateTime endDate,
|
||||||
@Nullable List<State.Type> state
|
@Nullable List<State.Type> state,
|
||||||
|
int batchSize
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
PurgeResult purgeResult = this.executionRepository
|
PurgeResult purgeResult = this.executionRepository
|
||||||
.find(
|
.find(
|
||||||
@@ -448,24 +467,27 @@ public class ExecutionService {
|
|||||||
null,
|
null,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
.map(throwFunction(execution -> {
|
.buffer(batchSize)
|
||||||
|
.map(throwFunction(executions -> {
|
||||||
PurgeResult.PurgeResultBuilder<?, ?> builder = PurgeResult.builder();
|
PurgeResult.PurgeResultBuilder<?, ?> builder = PurgeResult.builder();
|
||||||
|
|
||||||
if (purgeExecution) {
|
if (purgeExecution) {
|
||||||
builder.executionsCount(this.executionRepository.purge(execution));
|
builder.executionsCount(this.executionRepository.purge(executions));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (purgeLog) {
|
if (purgeLog) {
|
||||||
builder.logsCount(this.logRepository.purge(execution));
|
builder.logsCount(this.logRepository.purge(executions));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (purgeMetric) {
|
if (purgeMetric) {
|
||||||
builder.metricsCount(this.metricRepository.purge(execution));
|
builder.metricsCount(this.metricRepository.purge(executions));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (purgeStorage) {
|
if (purgeStorage) {
|
||||||
URI uri = StorageContext.forExecution(execution).getExecutionStorageURI(StorageContext.KESTRA_SCHEME);
|
executions.forEach(throwConsumer(execution -> {
|
||||||
builder.storagesCount(storageInterface.deleteByPrefix(execution.getTenantId(), execution.getNamespace(), uri).size());
|
URI uri = StorageContext.forExecution(execution).getExecutionStorageURI(StorageContext.KESTRA_SCHEME);
|
||||||
|
builder.storagesCount(storageInterface.deleteByPrefix(execution.getTenantId(), execution.getNamespace(), uri).size());
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (PurgeResult) builder.build();
|
return (PurgeResult) builder.build();
|
||||||
@@ -706,7 +728,7 @@ public class ExecutionService {
|
|||||||
// An edge case can exist where the execution is resumed automatically before we resume it with a killing.
|
// An edge case can exist where the execution is resumed automatically before we resume it with a killing.
|
||||||
try {
|
try {
|
||||||
newExecution = this.resume(execution, flow, State.Type.KILLING, null);
|
newExecution = this.resume(execution, flow, State.Type.KILLING, null);
|
||||||
newExecution = newExecution.withState(afterKillState.orElse(newExecution.getState().getCurrent()));
|
newExecution = newExecution.withState(killingOrAfterKillState);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// if we cannot resume, we set it anyway to killing, so we don't throw
|
// if we cannot resume, we set it anyway to killing, so we don't throw
|
||||||
log.warn("Unable to resume a paused execution before killing it", e);
|
log.warn("Unable to resume a paused execution before killing it", e);
|
||||||
@@ -715,10 +737,12 @@ public class ExecutionService {
|
|||||||
} else {
|
} else {
|
||||||
newExecution = execution.withState(killingOrAfterKillState);
|
newExecution = execution.withState(killingOrAfterKillState);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventPublisher.publishEvent(new CrudEvent<>(newExecution, execution, CrudEventType.UPDATE));
|
// Because this method is expected to be called by the Executor we can return the Execution
|
||||||
|
// immediately without publishing a CrudEvent like it's done on pause/resume method.
|
||||||
return newExecution;
|
return newExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Execution kill(Execution execution, FlowInterface flow) {
|
public Execution kill(Execution execution, FlowInterface flow) {
|
||||||
return this.kill(execution, flow, Optional.empty());
|
return this.kill(execution, flow, Optional.empty());
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user