mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 20:00:14 -05:00
Compare commits
30 Commits
dependabot
...
v0.21.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86aec88de4 | ||
|
|
f609d57a0c | ||
|
|
f3852a3c24 | ||
|
|
804ff6a81c | ||
|
|
7869f90edd | ||
|
|
2b72306b3d | ||
|
|
f0d5d4b93f | ||
|
|
4e4ab80b2f | ||
|
|
c33d08afda | ||
|
|
a246ac38f5 | ||
|
|
7bdaa81dee | ||
|
|
6a1d831849 | ||
|
|
95d2d1dfa3 | ||
|
|
d12dd179c2 | ||
|
|
ceda5eb8ee | ||
|
|
1301aaac76 | ||
|
|
5f7468a9a4 | ||
|
|
aa24c888a3 | ||
|
|
c792d9b6ea | ||
|
|
a921b95404 | ||
|
|
e46df069a9 | ||
|
|
c08f4f24ca | ||
|
|
67b3937824 | ||
|
|
17e1623342 | ||
|
|
d12fbf05b0 | ||
|
|
efa2d44e76 | ||
|
|
acdb46cea0 | ||
|
|
c1807516f5 | ||
|
|
ab796dff93 | ||
|
|
2d98f909de |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -9,6 +9,8 @@ jobs:
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
name: Check & Publish
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
24
.github/workflows/docker.yml
vendored
24
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Create Docker images on tag
|
||||
name: Create Docker images on Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
release-tag:
|
||||
description: 'Kestra Release Tag'
|
||||
required: false
|
||||
type: string
|
||||
plugin-version:
|
||||
description: 'Plugin version'
|
||||
required: false
|
||||
@@ -38,7 +42,6 @@ jobs:
|
||||
name: Publish Docker
|
||||
needs: [ plugins ]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
strategy:
|
||||
matrix:
|
||||
image:
|
||||
@@ -57,10 +60,19 @@ jobs:
|
||||
- name: Set image name
|
||||
id: vars
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/*/}
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
|
||||
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 [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then
|
||||
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Download release
|
||||
- name: Download release
|
||||
uses: robinraju/release-downloader@v1.11
|
||||
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -35,6 +35,8 @@ env:
|
||||
DOCKER_APT_PACKAGES: python3 python3-venv python-is-python3 python3-pip nodejs npm curl zip unzip
|
||||
DOCKER_PYTHON_LIBRARIES: kestra
|
||||
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build Artifacts
|
||||
@@ -45,13 +47,14 @@ jobs:
|
||||
docker-artifact-name: ${{ steps.vars.outputs.artifact }}
|
||||
plugins: ${{ steps.plugins-list.outputs.plugins }}
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout current ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout GitHub Actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
|
||||
11
.github/workflows/release-plugins.yml
vendored
11
.github/workflows/release-plugins.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseVersion:
|
||||
description: 'The release version (e.g., 0.21.0)'
|
||||
description: 'The release version (e.g., 0.21.0-RC1)'
|
||||
required: true
|
||||
type: string
|
||||
nextVersion:
|
||||
@@ -25,6 +25,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 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
|
||||
|
||||
15
.github/workflows/tag-plugins.yml
vendored
15
.github/workflows/tag-plugins.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Update and Tag Kestra Plugins
|
||||
name: Set Version and Tag Plugins
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -21,6 +21,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 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
|
||||
@@ -30,7 +39,7 @@ jobs:
|
||||
plugin-version: 'LATEST'
|
||||
|
||||
# Execute
|
||||
- name: Tag Plugins
|
||||
- name: Set Version and Tag Plugins
|
||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
@@ -41,7 +50,7 @@ jobs:
|
||||
--yes \
|
||||
${{ steps.plugins-list.outputs.repositories }}
|
||||
|
||||
- name: Run Gradle Release (DRY_RUN)
|
||||
- name: Set Version and Tag Plugins (DRY_RUN)
|
||||
if: ${{ github.event.inputs.dryRun == 'true' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.services.FlowListenersInterface;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
|
||||
@@ -36,6 +37,9 @@ public class FileChangedEventListener {
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
|
||||
@Inject
|
||||
private PluginDefaultService pluginDefaultService;
|
||||
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@@ -64,7 +68,7 @@ public class FileChangedEventListener {
|
||||
|
||||
public void startListeningFromConfig() throws IOException, InterruptedException {
|
||||
if (fileWatchConfiguration != null && fileWatchConfiguration.isEnabled()) {
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface);
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface, pluginDefaultService);
|
||||
List<Path> paths = fileWatchConfiguration.getPaths();
|
||||
this.setup(paths);
|
||||
|
||||
@@ -107,7 +111,6 @@ public class FileChangedEventListener {
|
||||
} else {
|
||||
log.info("File watching is disabled.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void startListening(List<Path> paths) throws IOException, InterruptedException {
|
||||
@@ -118,60 +121,64 @@ public class FileChangedEventListener {
|
||||
WatchKey key;
|
||||
while ((key = watchService.take()) != null) {
|
||||
for (WatchEvent<?> watchEvent : key.pollEvents()) {
|
||||
WatchEvent.Kind<?> kind = watchEvent.kind();
|
||||
Path entry = (Path) watchEvent.context();
|
||||
try {
|
||||
WatchEvent.Kind<?> kind = watchEvent.kind();
|
||||
Path entry = (Path) watchEvent.context();
|
||||
|
||||
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
|
||||
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
|
||||
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
if (Files.isDirectory(filePath)) {
|
||||
loadFlowsFromFolder(filePath);
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
if (Files.isDirectory(filePath)) {
|
||||
loadFlowsFromFolder(filePath);
|
||||
} else {
|
||||
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
|
||||
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
|
||||
// Check if Flow from file has id/namespace updated
|
||||
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
|
||||
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
|
||||
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
|
||||
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
|
||||
// Check if Flow from file has id/namespace updated
|
||||
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
|
||||
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
|
||||
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
}
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("File not found: {}", entry, e);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading file: {}", entry, e);
|
||||
}
|
||||
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("File not found: {}", entry, e);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading file: {}", entry, e);
|
||||
}
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
flows.stream()
|
||||
.filter(flow -> flow.getPath().equals(filePath.toString()))
|
||||
.findFirst()
|
||||
.ifPresent(flowWithPath -> {
|
||||
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
|
||||
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
flows.stream()
|
||||
.filter(flow -> flow.getPath().equals(filePath.toString()))
|
||||
.findFirst()
|
||||
.ifPresent(flowWithPath -> {
|
||||
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
|
||||
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while watching flows", e);
|
||||
}
|
||||
}
|
||||
key.reset();
|
||||
@@ -230,7 +237,8 @@ public class FileChangedEventListener {
|
||||
private Optional<Flow> parseFlow(String content, Path entry) {
|
||||
try {
|
||||
Flow flow = yamlParser.parse(content, Flow.class);
|
||||
modelValidator.validate(flow);
|
||||
FlowWithSource withPluginDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
modelValidator.validate(withPluginDefault);
|
||||
return Optional.of(flow);
|
||||
} catch (ConstraintViolationException e) {
|
||||
log.warn("Error while parsing flow: {}", entry, e);
|
||||
|
||||
@@ -3,32 +3,36 @@ package io.kestra.cli.services;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Requires(property = "micronaut.io.watch.enabled", value = "true")
|
||||
@Slf4j
|
||||
public class LocalFlowFileWatcher implements FlowFilesManager {
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
private final FlowRepositoryInterface flowRepository;
|
||||
private final PluginDefaultService pluginDefaultService;
|
||||
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepositoryInterface) {
|
||||
this.flowRepositoryInterface = flowRepositoryInterface;
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepository, PluginDefaultService pluginDefaultService) {
|
||||
this.flowRepository = flowRepository;
|
||||
this.pluginDefaultService = pluginDefaultService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowWithSource createOrUpdateFlow(Flow flow, String content) {
|
||||
return flowRepositoryInterface.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepositoryInterface.update(flow, previous, content, flow))
|
||||
.orElseGet(() -> flowRepositoryInterface.create(flow, content, flow));
|
||||
FlowWithSource withDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
return flowRepository.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepository.update(flow, previous, content, withDefault))
|
||||
.orElseGet(() -> flowRepository.create(flow, content, withDefault));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFlow(FlowWithSource toDelete) {
|
||||
flowRepositoryInterface.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepositoryInterface::delete);
|
||||
log.error("Flow {} has been deleted", toDelete.getId());
|
||||
flowRepository.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepository::delete);
|
||||
log.info("Flow {} has been deleted", toDelete.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFlow(String tenantId, String namespace, String id) {
|
||||
flowRepositoryInterface.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepositoryInterface::delete);
|
||||
log.error("Flow {} has been deleted", id);
|
||||
flowRepository.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepository::delete);
|
||||
log.info("Flow {} has been deleted", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package io.kestra.cli.services;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.utils.Await;
|
||||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
@MicronautTest(environments = {"test", "file-watch"}, transactional = false)
|
||||
class FileChangedEventListenerTest {
|
||||
public static final String FILE_WATCH = "build/file-watch";
|
||||
@Inject
|
||||
private FileChangedEventListener fileWatcher;
|
||||
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepository;
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws IOException {
|
||||
if (!Files.exists(Path.of(FILE_WATCH))) {
|
||||
Files.createDirectories(Path.of(FILE_WATCH));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() throws IOException {
|
||||
if (Files.exists(Path.of(FILE_WATCH))) {
|
||||
FileUtils.deleteDirectory(Path.of(FILE_WATCH).toFile());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Exception {
|
||||
if (started.compareAndSet(false, true)) {
|
||||
executorService.execute(throwRunnable(() -> fileWatcher.startListeningFromConfig()));
|
||||
}
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but always pass locally
|
||||
void test() throws IOException, TimeoutException {
|
||||
// remove the flow if it already exists
|
||||
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "myflow").ifPresent(flow -> flowRepository.delete(flow));
|
||||
|
||||
// create a basic flow
|
||||
String flow = """
|
||||
id: myflow
|
||||
namespace: io.kestra.tests.watch
|
||||
|
||||
tasks:
|
||||
- id: hello
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello World! 🚀
|
||||
""";
|
||||
Files.write(Path.of(FILE_WATCH + "/myflow.yaml"), flow.getBytes());
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isPresent(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
Flow myflow = flowRepository.findById(null, "io.kestra.tests.watch", "myflow").orElseThrow();
|
||||
assertThat(myflow.getTasks(), hasSize(1));
|
||||
assertThat(myflow.getTasks().getFirst().getId(), is("hello"));
|
||||
assertThat(myflow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
|
||||
|
||||
// delete the flow
|
||||
Files.delete(Path.of(FILE_WATCH + "/myflow.yaml"));
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isEmpty(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but always pass locally
|
||||
void testWithPluginDefault() throws IOException, TimeoutException {
|
||||
// remove the flow if it already exists
|
||||
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "pluginDefault").ifPresent(flow -> flowRepository.delete(flow));
|
||||
|
||||
// create a flow with plugin default
|
||||
String pluginDefault = """
|
||||
id: pluginDefault
|
||||
namespace: io.kestra.tests.watch
|
||||
|
||||
tasks:
|
||||
- id: helloWithDefault
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
|
||||
pluginDefaults:
|
||||
- type: io.kestra.plugin.core.log.Log
|
||||
values:
|
||||
message: Hello World!
|
||||
""";
|
||||
Files.write(Path.of(FILE_WATCH + "/plugin-default.yaml"), pluginDefault.getBytes());
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isPresent(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
Flow pluginDefaultFlow = flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").orElseThrow();
|
||||
assertThat(pluginDefaultFlow.getTasks(), hasSize(1));
|
||||
assertThat(pluginDefaultFlow.getTasks().getFirst().getId(), is("helloWithDefault"));
|
||||
assertThat(pluginDefaultFlow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
|
||||
|
||||
// delete both files
|
||||
Files.delete(Path.of(FILE_WATCH + "/plugin-default.yaml"));
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isEmpty(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
}
|
||||
12
cli/src/test/resources/application-file-watch.yml
Normal file
12
cli/src/test/resources/application-file-watch.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
micronaut:
|
||||
io:
|
||||
watch:
|
||||
enabled: true
|
||||
paths:
|
||||
- build/file-watch
|
||||
|
||||
kestra:
|
||||
repository:
|
||||
type: memory
|
||||
queue:
|
||||
type: memory
|
||||
@@ -40,6 +40,10 @@ public class Plugin {
|
||||
private String subGroup;
|
||||
|
||||
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup) {
|
||||
return Plugin.of(registeredPlugin, subgroup, true);
|
||||
}
|
||||
|
||||
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup, boolean includeDeprecated) {
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.name = registeredPlugin.name();
|
||||
PluginSubGroup subGroupInfos = null;
|
||||
@@ -80,17 +84,17 @@ public class Plugin {
|
||||
|
||||
plugin.subGroup = subgroup;
|
||||
|
||||
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.apps = filterAndGetClassName(registeredPlugin.getApps()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.apps = filterAndGetClassName(registeredPlugin.getApps(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -100,12 +104,14 @@ public class Plugin {
|
||||
* Those classes are only filtered from the documentation to ensure backward compatibility.
|
||||
*
|
||||
* @param list The list of classes?
|
||||
* @param includeDeprecated whether to include deprecated plugins or not
|
||||
* @return a filtered streams.
|
||||
*/
|
||||
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list) {
|
||||
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list, boolean includeDeprecated) {
|
||||
return list
|
||||
.stream()
|
||||
.filter(not(io.kestra.core.models.Plugin::isInternal))
|
||||
.filter(p -> includeDeprecated || !io.kestra.core.models.Plugin.isDeprecated(p))
|
||||
.map(Class::getName)
|
||||
.filter(c -> !c.startsWith("org.kestra."))
|
||||
.toList();
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
||||
import io.kestra.core.services.ExecutionService;
|
||||
import io.kestra.core.storages.Storage;
|
||||
import io.kestra.core.trace.TracerFactory;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import io.kestra.core.trace.propagation.ExecutionTextMapSetter;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
@@ -153,7 +154,7 @@ public final class ExecutableUtils {
|
||||
throw new IllegalStateException("Cannot execute an invalid flow: " + fwe.getException());
|
||||
}
|
||||
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(currentExecution.getLabels()) : new ArrayList<>(systemLabels(currentExecution));
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
|
||||
if (labels != null) {
|
||||
labels.forEach(throwConsumer(label -> newLabels.add(new Label(runContext.render(label.key()), runContext.render(label.value())))));
|
||||
}
|
||||
@@ -201,6 +202,16 @@ public final class ExecutableUtils {
|
||||
}));
|
||||
}
|
||||
|
||||
private static List<Label> filterLabels(List<Label> labels, Flow flow) {
|
||||
if (ListUtils.isEmpty(flow.getLabels())) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
return labels.stream()
|
||||
.filter(label -> flow.getLabels().stream().noneMatch(flowLabel -> flowLabel.key().equals(label.key())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<Label> systemLabels(Execution execution) {
|
||||
return Streams.of(execution.getLabels())
|
||||
.filter(label -> label.key().startsWith(Label.SYSTEM_PREFIX))
|
||||
|
||||
@@ -172,13 +172,15 @@ public class FlowService {
|
||||
subFlows.forEach(subflow -> {
|
||||
Optional<Flow> optional = findById(flow.getTenantId(), subflow.getNamespace(), subflow.getFlowId());
|
||||
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
|
||||
flow,
|
||||
Flow.class,
|
||||
"flow.tasks",
|
||||
flow.getNamespace()
|
||||
));
|
||||
if (optional.isEmpty()) {
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
|
||||
flow,
|
||||
Flow.class,
|
||||
"flow.tasks",
|
||||
flow.getNamespace()
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KestraTest
|
||||
@@ -323,7 +324,7 @@ class FlowServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkValidSubflowsNotFound() {
|
||||
void checkSubflowNotFound() {
|
||||
Flow flow = create("mainFlow", "task", 1).toBuilder()
|
||||
.tasks(List.of(
|
||||
io.kestra.plugin.core.flow.Subflow.builder()
|
||||
@@ -342,4 +343,23 @@ class FlowServiceTest {
|
||||
assertThat(exception.getConstraintViolations().size(), is(1));
|
||||
assertThat(exception.getConstraintViolations().iterator().next().getMessage(), is("The subflow 'nonExistentSubflow' not found in namespace 'io.kestra.unittest'."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkValidSubflow() {
|
||||
Flow subflow = create("existingSubflow", "task", 1);
|
||||
flowRepository.create(subflow, subflow.generateSource(), subflow);
|
||||
|
||||
Flow flow = create("mainFlow", "task", 1).toBuilder()
|
||||
.tasks(List.of(
|
||||
io.kestra.plugin.core.flow.Subflow.builder()
|
||||
.id("subflowTask")
|
||||
.type(io.kestra.plugin.core.flow.Subflow.class.getName())
|
||||
.namespace("io.kestra.unittest")
|
||||
.flowId("existingSubflow")
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> flowService.checkValidSubflows(flow));
|
||||
}
|
||||
}
|
||||
@@ -99,21 +99,24 @@ public class FlowCaseTest {
|
||||
assertThat(triggered.get().getState().getCurrent(), is(triggerState));
|
||||
|
||||
if (testInherited) {
|
||||
assertThat(triggered.get().getLabels().size(), is(5));
|
||||
assertThat(triggered.get().getLabels().size(), is(6));
|
||||
assertThat(triggered.get().getLabels(), hasItems(
|
||||
new Label(Label.CORRELATION_ID, execution.getId()),
|
||||
new Label("mainFlowExecutionLabel", "execFoo"),
|
||||
new Label("mainFlowLabel", "flowFoo"),
|
||||
new Label("launchTaskLabel", "launchFoo"),
|
||||
new Label("switchFlowLabel", "switchFoo")
|
||||
new Label("switchFlowLabel", "switchFoo"),
|
||||
new Label("overriding", "child")
|
||||
));
|
||||
} else {
|
||||
assertThat(triggered.get().getLabels().size(), is(3));
|
||||
assertThat(triggered.get().getLabels().size(), is(4));
|
||||
assertThat(triggered.get().getLabels(), hasItems(
|
||||
new Label(Label.CORRELATION_ID, execution.getId()),
|
||||
new Label("launchTaskLabel", "launchFoo"),
|
||||
new Label("switchFlowLabel", "switchFoo")
|
||||
new Label("switchFlowLabel", "switchFoo"),
|
||||
new Label("overriding", "child")
|
||||
));
|
||||
assertThat(triggered.get().getLabels(), not(hasItems(new Label("inherited", "label"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ public class ForEachItemCaseTest {
|
||||
}
|
||||
|
||||
public void restartForEachItem() throws Exception {
|
||||
CountDownLatch countDownLatch = new CountDownLatch(26);
|
||||
CountDownLatch countDownLatch = new CountDownLatch(6);
|
||||
Flux<Execution> receiveSubflows = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution subflowExecution = either.getLeft();
|
||||
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isFailed()) {
|
||||
@@ -285,7 +285,7 @@ public class ForEachItemCaseTest {
|
||||
});
|
||||
|
||||
URI file = storageUpload();
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 4);
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 20);
|
||||
Execution execution = runnerUtils.runOne(null, TEST_NAMESPACE, "restart-for-each-item", null,
|
||||
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
||||
Duration.ofSeconds(30));
|
||||
@@ -296,7 +296,7 @@ public class ForEachItemCaseTest {
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
|
||||
receiveSubflows.blockLast();
|
||||
|
||||
CountDownLatch successLatch = new CountDownLatch(26);
|
||||
CountDownLatch successLatch = new CountDownLatch(6);
|
||||
receiveSubflows = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution subflowExecution = either.getLeft();
|
||||
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isSuccess()) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -43,7 +43,7 @@ class TimeoutTest {
|
||||
@Inject
|
||||
private RunnerUtils runnerUtils;
|
||||
|
||||
@Test
|
||||
@RetryingTest(5) // Flaky on CI but never locally even with 100 repetitions
|
||||
void timeout() throws TimeoutException, QueueException {
|
||||
List<LogEntry> logs = new CopyOnWriteArrayList<>();
|
||||
Flux<LogEntry> receive = TestsUtils.receive(workerTaskLogQueue, either -> logs.add(either.getLeft()));
|
||||
|
||||
@@ -10,6 +10,7 @@ inputs:
|
||||
|
||||
labels:
|
||||
switchFlowLabel: switchFoo
|
||||
overriding: child
|
||||
|
||||
tasks:
|
||||
- id: parent-seq
|
||||
|
||||
@@ -7,6 +7,7 @@ inputs:
|
||||
|
||||
labels:
|
||||
mainFlowLabel: flowFoo
|
||||
overriding: parent
|
||||
|
||||
tasks:
|
||||
- id: launch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.21.0-rc0-SNAPSHOT
|
||||
version=0.21.0-rc1-SNAPSHOT
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.priority=low
|
||||
org.gradle.priority=low
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.kestra.core.models.annotations.Plugin.Id;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -55,6 +53,18 @@ public interface Plugin {
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to check whether a given plugin is deprecated.
|
||||
*
|
||||
* @param plugin The plugin type.
|
||||
* @return {@code true} if the plugin is deprecated.
|
||||
*/
|
||||
static boolean isDeprecated(final Class<?> plugin) {
|
||||
Objects.requireNonNull(plugin, "Cannot check if a plugin is deprecated from null");
|
||||
Deprecated annotation = plugin.getAnnotation(Deprecated.class);
|
||||
return annotation != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to get the id of a plugin.
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ javaPlatform {
|
||||
dependencies {
|
||||
// versions for libraries with multiple module but no BOM
|
||||
def slf4jVersion = "2.0.16"
|
||||
def protobufVersion = "4.29.3"
|
||||
def protobufVersion = "3.25.5" // Orc still uses 3.25.5 see https://github.com/apache/orc/blob/main/java/pom.xml
|
||||
def bouncycastleVersion = "1.80"
|
||||
def aetherVersion = "1.1.0"
|
||||
def jollydayVersion = "0.32.0"
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT
|
||||
# To release a specific plugin:
|
||||
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT plugin-kubernetes
|
||||
|
||||
# To release specific plugins from file:
|
||||
# ./release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -43,6 +44,7 @@ usage() {
|
||||
echo "Options:"
|
||||
echo " --release-version <version> Specify the release version (required)."
|
||||
echo " --next-version <version> Specify the next version (required)."
|
||||
echo " --plugin-file File containing the plugin list (default: .plugins)"
|
||||
echo " --dry-run Specify to run in DRY_RUN."
|
||||
echo " -y, --yes Automatically confirm prompts (non-interactive)."
|
||||
echo " -h, --help Show this help message and exit."
|
||||
@@ -81,6 +83,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
NEXT_VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
|
||||
@@ -143,7 +143,7 @@ public abstract class AbstractExecScript extends Task implements RunnableTask<Sc
|
||||
* protected DockerOptions docker = DockerOptions.builder().build();
|
||||
* }</pre>
|
||||
*/
|
||||
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) {
|
||||
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) throws IllegalVariableEvaluationException {
|
||||
// FIXME to keep backward compatibility, we call the old method from the new one by default
|
||||
return injectDefaults(original);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0
|
||||
# To release a specific plugin:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 plugin-kubernetes
|
||||
|
||||
# To release specific plugins from file:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -40,6 +41,7 @@ usage() {
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " --release-version <version> Specify the release version (required)."
|
||||
echo " --plugin-file File containing the plugin list (default: .plugins)"
|
||||
echo " --dry-run Specify to run in DRY_RUN."
|
||||
echo " -y, --yes Automatically confirm prompts (non-interactive)."
|
||||
echo " -h, --help Show this help message and exit."
|
||||
@@ -70,6 +72,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
RELEASE_VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
@@ -163,7 +173,7 @@ do
|
||||
git checkout "$RELEASE_BRANCH";
|
||||
|
||||
# Update version
|
||||
sed -i.bak "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
git add ./gradle.properties
|
||||
git commit -m"chore(version): update to version 'v$RELEASE_VERSION'."
|
||||
git push
|
||||
|
||||
@@ -6,7 +6,6 @@ Welcome to the Custom Dashboard! This feature allows you to create and manage pe
|
||||
|
||||
Below is an example of a dashboard definition that displays executions over time, a table that uses metrics to display the sum of sales per namespace, and a table that shows the log count by level per namespace:
|
||||
|
||||
::collapse{title="Expand for a example dashboard definition"}
|
||||
```yaml
|
||||
title: Getting Started
|
||||
description: First custom dashboard
|
||||
@@ -84,7 +83,6 @@ charts:
|
||||
- dev_graph
|
||||
- prod_graph
|
||||
```
|
||||
::
|
||||
|
||||
To see all available properties to configure a custom dashboard as code, see examples provided in the [Enterprise Edition Examples](https://github.com/kestra-io/enterprise-edition-examples) repository.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:metadata
|
||||
@update-metadata="(k, v) => emits('updateMetadata', {[k]: v})"
|
||||
@update-task="(yaml) => emits('updateTask', yaml)"
|
||||
@reorder="(yaml) => emits('reorder', yaml)"
|
||||
@update-documentation="(task) => emits('updateDocumentation', task)"
|
||||
/>
|
||||
</div>
|
||||
@@ -30,6 +31,7 @@
|
||||
"updateTask",
|
||||
"updateMetadata",
|
||||
"updateDocumentation",
|
||||
"reorder",
|
||||
]);
|
||||
const props = defineProps({
|
||||
flow: {type: String, required: true},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class="item"
|
||||
@click="
|
||||
(store.commit('code/removeBreadcrumb', {position: index}),
|
||||
store.commit('code/unsetPanel'))
|
||||
store.commit('code/unsetPanel', false))
|
||||
"
|
||||
>
|
||||
<router-link :to="breadcrumb.to">
|
||||
|
||||
@@ -11,15 +11,22 @@
|
||||
<Creation :section="item.title" />
|
||||
</template>
|
||||
|
||||
<template v-if="creation">
|
||||
<Element
|
||||
v-for="(element, elementIndex) in item.elements"
|
||||
:key="elementIndex"
|
||||
:section="item.title"
|
||||
:element
|
||||
@remove-element="removeElement(item.title, elementIndex)"
|
||||
/>
|
||||
</template>
|
||||
<Element
|
||||
v-for="(element, elementIndex) in item.elements"
|
||||
:key="elementIndex"
|
||||
:section="item.title"
|
||||
:element
|
||||
@remove-element="removeElement(item.title, elementIndex)"
|
||||
@move-element="
|
||||
(direction: 'up' | 'down') =>
|
||||
moveElement(
|
||||
item.elements,
|
||||
element.id,
|
||||
elementIndex,
|
||||
direction,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
@@ -32,7 +39,7 @@
|
||||
import Creation from "./buttons/Creation.vue";
|
||||
import Element from "./Element.vue";
|
||||
|
||||
const emits = defineEmits(["remove"]);
|
||||
const emits = defineEmits(["remove", "reorder"]);
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
@@ -67,6 +74,27 @@
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
import {YamlUtils as YAML_FROM_UI_LIBS} from "@kestra-io/ui-libs";
|
||||
const moveElement = (
|
||||
items: Record<string, any>[] | undefined,
|
||||
elementID: string,
|
||||
index: number,
|
||||
direction: "up" | "down",
|
||||
) => {
|
||||
if (!items || !props.flow) return;
|
||||
if (
|
||||
(direction === "up" && index === 0) ||
|
||||
(direction === "down" && index === items.length - 1)
|
||||
)
|
||||
return;
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
emits(
|
||||
"reorder",
|
||||
YAML_FROM_UI_LIBS.swapTasks(props.flow, elementID, items[newIndex].id),
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -14,17 +14,21 @@
|
||||
size="small"
|
||||
class="border-0"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<ChevronUp @click.prevent.stop="emits('moveElement', 'up')" />
|
||||
<ChevronDown @click.prevent.stop="emits('moveElement', 'down')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
|
||||
import {DeleteOutline} from "../../utils/icons";
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
|
||||
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
|
||||
const emits = defineEmits(["removeElement"]);
|
||||
const emits = defineEmits(["removeElement", "moveElement"]);
|
||||
|
||||
const props = defineProps({
|
||||
section: {type: String, required: true},
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
<span v-if="required" class="me-1 text-danger">*</span>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
<div class="mt-1 mb-2 wrapper" :class="props.class">
|
||||
<el-input v-model="input" @input="handleInput" :placeholder :disabled />
|
||||
<el-input
|
||||
v-model="input"
|
||||
@input="handleInput"
|
||||
:placeholder
|
||||
:disabled
|
||||
type="textarea"
|
||||
:autosize="{minRows: 1}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
creation
|
||||
:flow
|
||||
@remove="(yaml) => emits('updateTask', yaml)"
|
||||
@reorder="(yaml) => emits('reorder', yaml)"
|
||||
/>
|
||||
|
||||
<hr class="my-4">
|
||||
@@ -96,6 +97,7 @@
|
||||
"updateTask",
|
||||
"updateMetadata",
|
||||
"updateDocumentation",
|
||||
"reorder",
|
||||
]);
|
||||
|
||||
const saveEvent = (e: KeyboardEvent) => {
|
||||
@@ -235,6 +237,7 @@
|
||||
"error_handlers",
|
||||
YamlUtils.parse(props.flow).errors ?? [],
|
||||
),
|
||||
getSectionTitle("finally", YamlUtils.parse(props.flow).finally ?? []),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
v-else
|
||||
:is="lastBreadcumb.component.type"
|
||||
v-bind="lastBreadcumb.component.props"
|
||||
v-on="lastBreadcumb.component.listeners"
|
||||
:model-value="lastBreadcumb.component.props.modelValue"
|
||||
@update:model-value="validateTask"
|
||||
/>
|
||||
@@ -126,39 +125,51 @@
|
||||
YamlUtils.parse(yaml.value).id,
|
||||
);
|
||||
|
||||
if (route.query.section === SECTIONS.TRIGGERS.toLowerCase()) {
|
||||
const existingTask = YamlUtils.checkTaskAlreadyExist(
|
||||
source,
|
||||
CURRENT.value,
|
||||
);
|
||||
if (existingTask) {
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: "Trigger Id already exist",
|
||||
message: `Trigger Id ${existingTask} already exist in the flow.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const currentSection = route.query.section;
|
||||
const isCreation =
|
||||
props.creation &&
|
||||
(!route.query.identifier || route.query.identifier === "new");
|
||||
|
||||
emits("updateTask", YamlUtils.insertTrigger(source, CURRENT.value));
|
||||
CURRENT.value = null;
|
||||
} else {
|
||||
const action = props.creation
|
||||
? YamlUtils.insertTask(
|
||||
let result;
|
||||
|
||||
if (isCreation) {
|
||||
if (currentSection === "tasks") {
|
||||
const existing = YamlUtils.checkTaskAlreadyExist(
|
||||
source,
|
||||
YamlUtils.getLastTask(source),
|
||||
task,
|
||||
"after",
|
||||
)
|
||||
: YamlUtils.replaceTaskInDocument(
|
||||
source,
|
||||
route.query.identifier,
|
||||
task,
|
||||
CURRENT.value,
|
||||
);
|
||||
|
||||
emits("updateTask", action);
|
||||
if (existing) {
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: "Task with same ID already exist",
|
||||
message: `Task in ${route.query.section} block with ID: ${existing} already exist in the flow.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
result = YamlUtils.insertTask(
|
||||
source,
|
||||
route.query.target ?? YamlUtils.getLastTask(source),
|
||||
task,
|
||||
route.query.position ?? "after",
|
||||
);
|
||||
} else if (currentSection === "triggers") {
|
||||
result = YamlUtils.insertTrigger(source, CURRENT.value);
|
||||
} else if (currentSection === "error handlers") {
|
||||
result = YamlUtils.insertError(source, CURRENT.value);
|
||||
} else if (currentSection === "finally") {
|
||||
result = YamlUtils.insertFinally(source, CURRENT.value);
|
||||
}
|
||||
} else {
|
||||
result = YamlUtils.replaceTaskInDocument(
|
||||
source,
|
||||
route.query.identifier,
|
||||
task,
|
||||
);
|
||||
}
|
||||
|
||||
emits("updateTask", result);
|
||||
store.commit("code/removeBreadcrumb", {last: true});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Plus from "vue-material-design-icons/Plus.vue";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import DeleteOutline from "vue-material-design-icons/DeleteOutline.vue";
|
||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||
|
||||
export {Plus, ContentSave, DeleteOutline};
|
||||
export {Plus, ContentSave, DeleteOutline, ChevronUp, ChevronDown};
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
import {Utils} from "@kestra-io/ui-libs";
|
||||
import KestraUtils from "../../../../../utils/utils.js"
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@@ -129,7 +130,7 @@
|
||||
const parsedData = computed(() => {
|
||||
const parseValue = (value) => {
|
||||
const date = moment(value, moment.ISO_8601, true);
|
||||
return date.isValid() ? date.format("YYYY-MM-DD") : value;
|
||||
return date.isValid() ? date.format(KestraUtils.getDateFormat(route.query.startDate, route.query.endDate)) : value;
|
||||
};
|
||||
|
||||
const rawData = generated.value.results;
|
||||
|
||||
@@ -1,97 +1,51 @@
|
||||
<template>
|
||||
<el-row
|
||||
v-for="(item, index) in values"
|
||||
v-for="(element, index) in items"
|
||||
:key="'array-' + index"
|
||||
:gutter="10"
|
||||
class="w-100"
|
||||
class="w-100 mb-2"
|
||||
>
|
||||
<el-col :span="22">
|
||||
<component
|
||||
:is="`task-${getType(schema.items)}`"
|
||||
:model-value="item"
|
||||
@update:model-value="onInput(index, $event)"
|
||||
:root="getKey(index)"
|
||||
:schema="schema.items"
|
||||
:definitions="definitions"
|
||||
<InputText
|
||||
:model-value="element"
|
||||
@update:model-value="(v) => handleInput(v, index)"
|
||||
:placeholder="$t('value')"
|
||||
class="w-100"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="2" class="col align-self-center delete">
|
||||
<DeleteOutline @click="removeItem(key)" />
|
||||
<DeleteOutline @click="removeItem(index)" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<Add @add="addItem()" v-if="values.at(-1)" />
|
||||
<Add @add="addItem()" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
import {DeleteOutline} from "../../code/utils/icons";
|
||||
|
||||
import InputText from "../../code/components/inputs/InputText.vue";
|
||||
import Add from "../../code/components/Add.vue";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import {toRaw} from "vue";
|
||||
import Task from "./Task";
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({modelValue: {type: Array, default: undefined}});
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
emits: ["update:modelValue"],
|
||||
created() {
|
||||
if (!Array.isArray(this.modelValue) && this.modelValue !== undefined) {
|
||||
this.$emit("update:modelValue", []);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
values() {
|
||||
if (this.modelValue === undefined) {
|
||||
return this.schema.default || [undefined];
|
||||
}
|
||||
const items = ref(
|
||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||
);
|
||||
|
||||
return this.modelValue;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPropertiesValue(properties) {
|
||||
return this.modelValue && this.modelValue[properties]
|
||||
? this.modelValue[properties]
|
||||
: undefined;
|
||||
},
|
||||
onInput(index, value) {
|
||||
const local = this.modelValue || [];
|
||||
local[index] = value;
|
||||
const handleInput = (value: string, index: number) => {
|
||||
items.value[index] = value;
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
|
||||
this.$emit("update:modelValue", local);
|
||||
},
|
||||
addItem() {
|
||||
let local = this.modelValue || [];
|
||||
local.push(undefined);
|
||||
|
||||
// click on + when there is no items
|
||||
if (this.modelValue === undefined) {
|
||||
local.push(undefined);
|
||||
}
|
||||
|
||||
this.$emit("update:modelValue", local);
|
||||
},
|
||||
removeItem(x) {
|
||||
let local = this.modelValue || [];
|
||||
local.splice(x, 1);
|
||||
|
||||
if (local.length === 1) {
|
||||
let raw = toRaw(local[0]);
|
||||
|
||||
if (raw === null || raw === undefined) {
|
||||
local = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit("update:modelValue", local);
|
||||
},
|
||||
},
|
||||
const addItem = () => {
|
||||
items.value.push(undefined);
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
const removeItem = (index: number) => {
|
||||
items.value.splice(index, 1);
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../code/styles/code.scss";
|
||||
</style>
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
properties(requiredFields) {
|
||||
if (this.schema) {
|
||||
if (this.schema?.properties) {
|
||||
const properties = Object.entries(
|
||||
this.schema.properties,
|
||||
).reduce((acc, [key, value]) => {
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
:flow="flowYaml"
|
||||
@update-metadata="(e) => onUpdateMetadata(e, true)"
|
||||
@update-task="(e) => editorUpdate(e)"
|
||||
@reorder="(yaml) => handleReorder(yaml)"
|
||||
@update-documentation="(task) => updatePluginDocumentation(undefined, task)"
|
||||
/>
|
||||
</div>
|
||||
@@ -799,8 +800,8 @@
|
||||
}
|
||||
|
||||
haveChange.value = true;
|
||||
store.dispatch("core/isUnsaved", true);
|
||||
|
||||
if(editorViewType.value === "YAML") store.dispatch("core/isUnsaved", true);
|
||||
|
||||
if(!props.isCreating){
|
||||
store.commit("editor/changeOpenedTabs", {
|
||||
action: "dirty",
|
||||
@@ -959,6 +960,12 @@
|
||||
haveChange.value = true;
|
||||
};
|
||||
|
||||
const handleReorder = (yaml) => {
|
||||
flowYaml.value = yaml;
|
||||
haveChange.value = true;
|
||||
save()
|
||||
};
|
||||
|
||||
const editorUpdate = (event) => {
|
||||
const currentIsFlow = isFlow();
|
||||
|
||||
|
||||
@@ -293,10 +293,12 @@
|
||||
);
|
||||
};
|
||||
|
||||
const onCreateNewTask = () => {
|
||||
const onCreateNewTask = (details) => {
|
||||
emit("openNoCode", {
|
||||
section: SECTIONS.TASKS.toLowerCase(),
|
||||
identifier: "new",
|
||||
target: details[0],
|
||||
position: details[1],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@
|
||||
},
|
||||
methods: {
|
||||
loadToc() {
|
||||
this.$store.dispatch("plugin/listWithSubgroup")
|
||||
this.$store.dispatch("plugin/listWithSubgroup", {
|
||||
includeDeprecated: false
|
||||
})
|
||||
},
|
||||
|
||||
loadPlugin() {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<KestraFilter :placeholder="$t('pluginPage.search', {count: countPlugin})" :search-callback="(input)=> searchInput = input" />
|
||||
</el-row>
|
||||
<section class="px-3 plugins-container">
|
||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="index" effect="light">
|
||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="plugin.name + '-' + index" effect="light">
|
||||
<template #content>
|
||||
<div class="tasks-tooltips">
|
||||
<p v-if="plugin?.tasks.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||
|
||||
@@ -86,7 +86,9 @@
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
if(!this.$route?.params?.tab) this.$router.push({name: "blueprints", params: {tab: "community"}})
|
||||
if(!this.embed && !this.$route?.params?.tab) {
|
||||
this.$router.push({name: "blueprints", params: {tab: "community", kind: this.kind}})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routeInfo() {
|
||||
|
||||
@@ -61,6 +61,15 @@ export function useLeftMenu() {
|
||||
},
|
||||
exact: false,
|
||||
},
|
||||
{
|
||||
href: {name: "apps/list"},
|
||||
routes: routeStartWith("apps"),
|
||||
title: t("apps"),
|
||||
icon: {
|
||||
element: shallowRef(FormatListGroupPlus),
|
||||
class: "menu-icon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: {name: "templates/list"},
|
||||
routes: routeStartWith("templates"),
|
||||
@@ -145,15 +154,6 @@ export function useLeftMenu() {
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
{
|
||||
href: {name: "apps/list"},
|
||||
routes: routeStartWith("apps"),
|
||||
title: t("apps"),
|
||||
icon: {
|
||||
element: shallowRef(FormatListGroupPlus),
|
||||
class: "menu-icon"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("administration"),
|
||||
routes: routeStartWith("admin"),
|
||||
|
||||
@@ -41,9 +41,9 @@ export default {
|
||||
state.panel = panel;
|
||||
state.breadcrumbs[1] = {...breadcrumb, panel: true};
|
||||
},
|
||||
unsetPanel(state: State) {
|
||||
unsetPanel(state: State, shouldSplice = true) {
|
||||
state.panel = undefined;
|
||||
state.breadcrumbs.splice(1);
|
||||
if (shouldSplice) state.breadcrumbs.splice(1);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,8 +21,10 @@ export default {
|
||||
return response.data;
|
||||
})
|
||||
},
|
||||
listWithSubgroup({commit}) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/groups/subgroups`, {}).then(response => {
|
||||
listWithSubgroup({commit}, options) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/groups/subgroups`, {
|
||||
params: options
|
||||
}).then(response => {
|
||||
commit("setPlugins", response.data)
|
||||
commit("setPluginSingleList", response.data.map(plugin => plugin.tasks.concat(plugin.triggers, plugin.conditions, plugin.controllers, plugin.storages, plugin.taskRunners, plugin.charts, plugin.dataFilters, plugin.aliases, plugin.logExporters)).flat())
|
||||
return response.data;
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Haupteigenschaften",
|
||||
"error_handlers": "Fehlerbehandler",
|
||||
"tasks": "Aufgaben",
|
||||
"advanced": "Erweiterte Konfiguration"
|
||||
"advanced": "Erweiterte Konfiguration",
|
||||
"finally": "Schließlich"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Wählen Sie einen {section} aus",
|
||||
"tasks": "Aufgabe hinzufügen",
|
||||
"error handlers": "Fügen Sie einen Fehlerhandler hinzu",
|
||||
"triggers": "Füge einen Trigger hinzu"
|
||||
"triggers": "Füge einen Trigger hinzu",
|
||||
"finally": "Fügen Sie einen Finally-Block hinzu"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Aufgabe speichern",
|
||||
"triggers": "Speicher Trigger",
|
||||
"error handlers": "Fehlerbehandlungsroutine speichern",
|
||||
"input": "Eingabe speichern"
|
||||
"input": "Eingabe speichern",
|
||||
"finally": "Aufgabe speichern"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Kein Code-Editor",
|
||||
|
||||
@@ -821,18 +821,18 @@
|
||||
"edit": {
|
||||
"title": "Upgrade Your Namespace Management",
|
||||
"message": "In Kestra Enterprise Edition, namespaces provide advanced isolation and governance of secrets, variables and plugin defaults at scale. Administrators can configure custom secrets managers, isolated storage backends, dedicated worker groups, and fine-grained permissions on a per-namespace basis. This ensures that secrets, variables and plugin configurations remain secure and easy to maintain across different teams and projects."
|
||||
},
|
||||
},
|
||||
"variables": {
|
||||
"title": "Centrally Govern Your Variables",
|
||||
"message": "In Kestra Enterprise Edition, you can define and manage namespace-level variables to eliminate repetitive configurations across flows. These variables ensure consistency, simplify configuration updates, and can be easily referenced by any task or trigger for cleaner, more maintainable workflows."
|
||||
},
|
||||
"plugin-defaults": {
|
||||
"title": "Standardize Configuration with Plugin Defaults",
|
||||
"message": "In Kestra Enterprise Edition, you can set namespace-specific plugin defaults, reducing the need for duplicated setup in each flow. This central plugin governance enforces consistent configurations, allows secure referencing of secrets or variables, and simplifies maintenance of your workflows."
|
||||
},
|
||||
"secrets": {
|
||||
"title": "Manage Secrets in a Secure Way",
|
||||
"message": "In Kestra Enterprise Edition, you can store and control secrets at the namespace level, minimizing risk and ensuring each team's credentials remain isolated. Thanks to the nested hierarchy, you can also configure credentials in a parent namespace and they will be inherited by all child namespaces. Support for dedicated secrets managers and fine-grained namespace-level permission settings further strengthens security and compliance."
|
||||
"title": "Centrally Govern Your Variables",
|
||||
"message": "In Kestra Enterprise Edition, you can define and manage namespace-level variables to eliminate repetitive configurations across flows. These variables ensure consistency, simplify configuration updates, and can be easily referenced by any task or trigger for cleaner, more maintainable workflows."
|
||||
},
|
||||
"plugin-defaults": {
|
||||
"title": "Standardize Configuration with Plugin Defaults",
|
||||
"message": "In Kestra Enterprise Edition, you can set namespace-specific plugin defaults, reducing the need for duplicated setup in each flow. This central plugin governance enforces consistent configurations, allows secure referencing of secrets or variables, and simplifies maintenance of your workflows."
|
||||
},
|
||||
"secrets": {
|
||||
"title": "Manage Secrets in a Secure Way",
|
||||
"message": "In Kestra Enterprise Edition, you can store and control secrets at the namespace level, minimizing risk and ensuring each team's credentials remain isolated. Thanks to the nested hierarchy, you can also configure credentials in a parent namespace and they will be inherited by all child namespaces. Support for dedicated secrets managers and fine-grained namespace-level permission settings further strengthens security and compliance."
|
||||
},
|
||||
"audit-logs": {
|
||||
"title": "Track All Changes in One Place",
|
||||
@@ -1128,7 +1128,8 @@
|
||||
"advanced": "Advanced configuration",
|
||||
"tasks": "Tasks",
|
||||
"triggers": "Triggers",
|
||||
"error_handlers": "Error Handlers"
|
||||
"error_handlers": "Error Handlers",
|
||||
"finally": "Finally"
|
||||
},
|
||||
"fields": {
|
||||
"main": {
|
||||
@@ -1151,12 +1152,14 @@
|
||||
"select": "Select a {section}",
|
||||
"tasks": "Add a task",
|
||||
"triggers": "Add a trigger",
|
||||
"error handlers": "Add an error handler"
|
||||
"error handlers": "Add an error handler",
|
||||
"finally": "Add a finally block"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Save task",
|
||||
"triggers": "Save trigger",
|
||||
"error handlers": "Save error handler",
|
||||
"finally": "Save task",
|
||||
"input": "Save input"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Propiedades principales",
|
||||
"error_handlers": "Manejadores de Errores",
|
||||
"tasks": "Tareas",
|
||||
"advanced": "Configuración avanzada"
|
||||
"advanced": "Configuración avanzada",
|
||||
"finally": "Finalmente"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Seleccione una {section}",
|
||||
"tasks": "Agregar una task",
|
||||
"error handlers": "Agregar un manejador de errores",
|
||||
"triggers": "Agregar un trigger"
|
||||
"triggers": "Agregar un trigger",
|
||||
"finally": "Agregar un bloque finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Guardar tarea",
|
||||
"triggers": "Guardar trigger",
|
||||
"error handlers": "Guardar controlador de errores",
|
||||
"input": "Guardar input"
|
||||
"input": "Guardar input",
|
||||
"finally": "Guardar tarea"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Editor Sin Código",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Principales propriétés",
|
||||
"error_handlers": "Gestionnaires d'erreurs",
|
||||
"tasks": "Tâches",
|
||||
"advanced": "Configuration avancée"
|
||||
"advanced": "Configuration avancée",
|
||||
"finally": "Enfin"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Sélectionnez une {section}",
|
||||
"tasks": "Ajouter une tâche",
|
||||
"error handlers": "Ajouter un gestionnaire d'erreurs",
|
||||
"triggers": "Ajouter un trigger"
|
||||
"triggers": "Ajouter un trigger",
|
||||
"finally": "Ajouter un bloc finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Enregistrer la tâche",
|
||||
"triggers": "Enregistrer le trigger",
|
||||
"error handlers": "Enregistrer le gestionnaire d'erreurs",
|
||||
"input": "Enregistrer l'input"
|
||||
"input": "Enregistrer l'input",
|
||||
"finally": "Enregistrer la task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Éditeur sans code",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "मुख्य गुणधर्म",
|
||||
"error_handlers": "त्रुटि हैंडलर",
|
||||
"tasks": "कार्य",
|
||||
"advanced": "उन्नत कॉन्फ़िगरेशन"
|
||||
"advanced": "उन्नत कॉन्फ़िगरेशन",
|
||||
"finally": "अंत में"
|
||||
},
|
||||
"creation": {
|
||||
"select": "किसी {section} का चयन करें",
|
||||
"tasks": "कार्य जोड़ें",
|
||||
"error handlers": "त्रुटि हैंडलर जोड़ें",
|
||||
"triggers": "ट्रिगर जोड़ें"
|
||||
"triggers": "ट्रिगर जोड़ें",
|
||||
"finally": "अंत में एक finally ब्लॉक जोड़ें"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "कार्य सहेजें",
|
||||
"triggers": "ट्रिगर सहेजें",
|
||||
"error handlers": "त्रुटि हैंडलर सहेजें",
|
||||
"input": "इनपुट सहेजें"
|
||||
"input": "इनपुट सहेजें",
|
||||
"finally": "कार्य सहेजें"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "कोड संपादक नहीं",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Proprietà principali",
|
||||
"error_handlers": "Gestori degli Errori",
|
||||
"tasks": "Attività",
|
||||
"advanced": "Configurazione avanzata"
|
||||
"advanced": "Configurazione avanzata",
|
||||
"finally": "Infine"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Seleziona una {section}",
|
||||
"tasks": "Aggiungi un task",
|
||||
"error handlers": "Aggiungi un gestore degli errori",
|
||||
"triggers": "Aggiungi un trigger"
|
||||
"triggers": "Aggiungi un trigger",
|
||||
"finally": "Aggiungi un blocco finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Salva task",
|
||||
"triggers": "Salva trigger",
|
||||
"error handlers": "Salva gestore errori",
|
||||
"input": "Salva input"
|
||||
"input": "Salva input",
|
||||
"finally": "Salva task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Editor Senza Codice",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "メインプロパティ",
|
||||
"error_handlers": "エラーハンドラー",
|
||||
"tasks": "タスク",
|
||||
"advanced": "高度な設定"
|
||||
"advanced": "高度な設定",
|
||||
"finally": "最後に"
|
||||
},
|
||||
"creation": {
|
||||
"select": "{section}を選択してください",
|
||||
"tasks": "タスクを追加",
|
||||
"error handlers": "エラーハンドラーを追加",
|
||||
"triggers": "トリガーを追加"
|
||||
"triggers": "トリガーを追加",
|
||||
"finally": "finally ブロックを追加"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "タスクを保存",
|
||||
"triggers": "トリガーを保存",
|
||||
"error handlers": "エラーハンドラーを保存",
|
||||
"input": "入力を保存"
|
||||
"input": "入力を保存",
|
||||
"finally": "タスクを保存"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "コードエディタなし",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "주요 속성",
|
||||
"error_handlers": "오류 처리기",
|
||||
"tasks": "작업",
|
||||
"advanced": "고급 구성"
|
||||
"advanced": "고급 구성",
|
||||
"finally": "마지막으로"
|
||||
},
|
||||
"creation": {
|
||||
"select": "{section}을 선택하십시오",
|
||||
"tasks": "작업 추가",
|
||||
"error handlers": "오류 처리기 추가",
|
||||
"triggers": "트리거 추가"
|
||||
"triggers": "트리거 추가",
|
||||
"finally": "마지막 블록 추가"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "작업 저장",
|
||||
"triggers": "트리거 저장",
|
||||
"error handlers": "오류 처리기 저장",
|
||||
"input": "입력 저장"
|
||||
"input": "입력 저장",
|
||||
"finally": "작업 저장"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "코드 없는 편집기",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Główne właściwości",
|
||||
"error_handlers": "Obsługa błędów",
|
||||
"tasks": "Zadania",
|
||||
"advanced": "Zaawansowana konfiguracja"
|
||||
"advanced": "Zaawansowana konfiguracja",
|
||||
"finally": "Na koniec"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Wybierz {section}",
|
||||
"tasks": "Dodaj task",
|
||||
"error handlers": "Dodaj obsługę błędów",
|
||||
"triggers": "Dodaj trigger"
|
||||
"triggers": "Dodaj trigger",
|
||||
"finally": "Dodaj blok finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Zapisz task",
|
||||
"triggers": "Zapisz trigger",
|
||||
"error handlers": "Zapisz obsługę błędów",
|
||||
"input": "Zapisz input"
|
||||
"input": "Zapisz input",
|
||||
"finally": "Zapisz task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Edytor No Code",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Propriedades principais",
|
||||
"error_handlers": "Manipuladores de Erros",
|
||||
"tasks": "Tarefas",
|
||||
"advanced": "Configuração avançada"
|
||||
"advanced": "Configuração avançada",
|
||||
"finally": "Finalmente"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Selecione uma {section}",
|
||||
"tasks": "Adicionar uma task",
|
||||
"error handlers": "Adicionar um manipulador de erro",
|
||||
"triggers": "Adicionar um trigger"
|
||||
"triggers": "Adicionar um trigger",
|
||||
"finally": "Adicionar um bloco finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Salvar task",
|
||||
"triggers": "Salvar trigger",
|
||||
"error handlers": "Salvar manipulador de erro",
|
||||
"input": "Salvar input"
|
||||
"input": "Salvar input",
|
||||
"finally": "Salvar task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Editor Sem Código",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Основные свойства",
|
||||
"error_handlers": "Обработчики ошибок",
|
||||
"tasks": "Задачи",
|
||||
"advanced": "Расширенная конфигурация"
|
||||
"advanced": "Расширенная конфигурация",
|
||||
"finally": "Наконец"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Выберите {section}",
|
||||
"tasks": "Добавить task",
|
||||
"error handlers": "Добавить обработчик ошибок",
|
||||
"triggers": "Добавить trigger"
|
||||
"triggers": "Добавить trigger",
|
||||
"finally": "Добавить блок finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Сохранить task",
|
||||
"triggers": "Сохранить trigger",
|
||||
"error handlers": "Сохранить обработчик ошибок",
|
||||
"input": "Сохранить input"
|
||||
"input": "Сохранить input",
|
||||
"finally": "Сохранить task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Редактор No Code",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "主要属性",
|
||||
"error_handlers": "错误处理程序",
|
||||
"tasks": "任务",
|
||||
"advanced": "高级配置"
|
||||
"advanced": "高级配置",
|
||||
"finally": "最后"
|
||||
},
|
||||
"creation": {
|
||||
"select": "选择一个{section}",
|
||||
"tasks": "添加任务",
|
||||
"error handlers": "添加错误处理程序",
|
||||
"triggers": "添加一个trigger"
|
||||
"triggers": "添加一个trigger",
|
||||
"finally": "添加一个finally块"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "保存任务",
|
||||
"triggers": "保存 trigger",
|
||||
"error handlers": "保存错误处理程序",
|
||||
"input": "保存输入"
|
||||
"input": "保存输入",
|
||||
"finally": "保存任务"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "无代码编辑器",
|
||||
|
||||
@@ -286,4 +286,24 @@ export default class Utils {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
static getDateFormat(startDate, endDate) {
|
||||
if (!startDate || !endDate) {
|
||||
return "yyyy-MM-DD";
|
||||
}
|
||||
|
||||
const duration = moment.duration(moment(endDate).diff(moment(startDate)));
|
||||
|
||||
if (duration.asDays() > 365) {
|
||||
return "yyyy-MM";
|
||||
} else if (duration.asDays() > 180) {
|
||||
return "yyyy-'W'ww";
|
||||
} else if (duration.asDays() > 1) {
|
||||
return "yyyy-MM-DD";
|
||||
} else if (duration.asHours() > 1) {
|
||||
return "yyyy-MM-DD:HH:00";
|
||||
} else {
|
||||
return "yyyy-MM-DD:HH:mm";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,24 @@ export default class YamlUtils {
|
||||
return YamlUtils.cleanMetadata(yamlDoc.toString(TOSTRING_OPTIONS));
|
||||
}
|
||||
|
||||
static insertFinally(source, finallyTask) {
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
const newFinallyNode = yamlDoc.createNode(yaml.parseDocument(finallyTask));
|
||||
const items = yamlDoc.contents.items.find(item => item.key.value === "finally");
|
||||
if (items && items.value.items) {
|
||||
yamlDoc.contents.items[yamlDoc.contents.items.indexOf(items)].value.items.push(newFinallyNode);
|
||||
} else {
|
||||
if (items) {
|
||||
yamlDoc.contents.items.splice(yamlDoc.contents.items.indexOf(items), 1)
|
||||
}
|
||||
const finallySeq = new yaml.YAMLSeq();
|
||||
finallySeq.items.push(newFinallyNode);
|
||||
const newFinally = new yaml.Pair(new yaml.Scalar("finally"), finallySeq);
|
||||
yamlDoc.contents.items.push(newFinally);
|
||||
}
|
||||
return YamlUtils.cleanMetadata(yamlDoc.toString(TOSTRING_OPTIONS));
|
||||
}
|
||||
|
||||
static insertErrorInFlowable(source, errorTask, flowableTask) {
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
const newErrorNode = yamlDoc.createNode(yaml.parseDocument(errorTask));
|
||||
@@ -571,7 +589,7 @@ export default class YamlUtils {
|
||||
return source;
|
||||
}
|
||||
|
||||
const order = ["id", "namespace", "description", "retry", "labels", "inputs", "variables", "tasks", "triggers", "errors", "pluginDefaults", "taskDefaults", "concurrency", "outputs", "disabled"];
|
||||
const order = ["id", "namespace", "description", "retry", "labels", "inputs", "variables", "tasks", "triggers", "errors", "finally", "pluginDefaults", "taskDefaults", "concurrency", "outputs", "disabled"];
|
||||
const updatedItems = [];
|
||||
for (const prop of order) {
|
||||
const item = yamlDoc.contents.items.find(e => e.key.value === prop);
|
||||
|
||||
@@ -157,22 +157,23 @@ public class DashboardController {
|
||||
@Parameter(description = "The chart id") @PathVariable String chartId,
|
||||
@Parameter(description = "The filters to apply, some can override chart definition like labels & namespace") @Body GlobalFilter globalFilter
|
||||
) throws IOException {
|
||||
ZonedDateTime startDate = globalFilter.getStartDate();
|
||||
ZonedDateTime endDate = globalFilter.getEndDate();
|
||||
if (startDate == null || endDate == null) {
|
||||
throw new IllegalArgumentException("`startDate` and `endDate` filters are required.");
|
||||
}
|
||||
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("`endDate` must be after `startDate`.");
|
||||
}
|
||||
|
||||
String tenantId = tenantService.resolveTenant();
|
||||
Dashboard dashboard = dashboardRepository.get(tenantId, id).orElse(null);
|
||||
if (dashboard == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ZonedDateTime endDate = globalFilter.getEndDate();
|
||||
ZonedDateTime startDate = globalFilter.getStartDate();
|
||||
if (startDate == null || endDate == null) {
|
||||
endDate = ZonedDateTime.now();
|
||||
startDate = endDate.minus(dashboard.getTimeWindow().getDefaultDuration());
|
||||
}
|
||||
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("`endDate` must be after `startDate`.");
|
||||
}
|
||||
|
||||
Duration windowDuration = Duration.ofSeconds(endDate.minus(Duration.ofSeconds(startDate.toEpochSecond())).toEpochSecond());
|
||||
if (windowDuration.compareTo(dashboard.getTimeWindow().getMax()) > 0) {
|
||||
throw new IllegalArgumentException("The queried window is larger than the max allowed one.");
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
package io.kestra.webserver.controllers.api;
|
||||
|
||||
import io.kestra.core.docs.ClassInputDocumentation;
|
||||
import io.kestra.core.docs.ClassPluginDocumentation;
|
||||
import io.kestra.core.docs.DocumentationGenerator;
|
||||
import io.kestra.core.docs.DocumentationWithSchema;
|
||||
import io.kestra.core.docs.InputType;
|
||||
import io.kestra.core.docs.JsonSchemaGenerator;
|
||||
import io.kestra.core.docs.Plugin;
|
||||
import io.kestra.core.docs.PluginIcon;
|
||||
import io.kestra.core.docs.Schema;
|
||||
import io.kestra.core.docs.SchemaType;
|
||||
import io.kestra.core.docs.*;
|
||||
import io.kestra.core.models.dashboards.Dashboard;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.Input;
|
||||
@@ -39,11 +30,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -252,16 +239,18 @@ public class PluginController {
|
||||
@Get("/groups/subgroups")
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Operation(tags = {"Plugins"}, summary = "Get plugins group by subgroups")
|
||||
public List<Plugin> subgroups() {
|
||||
public List<Plugin> subgroups(
|
||||
@Parameter(description = "Whether to include deprecated plugins") @QueryValue(value = "includeDeprecated", defaultValue = "true") boolean includeDeprecated
|
||||
) {
|
||||
return Stream.concat(
|
||||
pluginRegistry.plugins()
|
||||
.stream()
|
||||
.map(p -> Plugin.of(p, null)),
|
||||
.map(p -> Plugin.of(p, null, includeDeprecated)),
|
||||
pluginRegistry.plugins()
|
||||
.stream()
|
||||
.flatMap(p -> p.subGroupNames()
|
||||
.stream()
|
||||
.map(subgroup -> Plugin.of(p, subgroup))
|
||||
.map(subgroup -> Plugin.of(p, subgroup, includeDeprecated))
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
|
||||
Reference in New Issue
Block a user