Compare commits

...

24 Commits

Author SHA1 Message Date
brian.mulier
83b7399171 chore(version): update to version 'v0.13.8'. 2023-12-22 14:38:25 +01:00
brian.mulier
d948e14319 chore(version): update to version 'v0.13.7'. 2023-12-21 16:47:30 +01:00
Loïc Mathieu
7143de9ec6 chore(version): update to version 'v0.13.6'. 2023-12-19 17:12:09 +01:00
Loïc Mathieu
33ece57996 chore(version): update to version 'v0.13.5'. 2023-12-14 10:08:09 +01:00
Loïc Mathieu
5804ff7b26 fix(core): skip execution for flows with no list of tasks (#2601) 2023-12-14 10:07:21 +01:00
YannC
2888d929aa chore(version): update to version 'v0.13.4'. 2023-11-30 22:40:35 +01:00
Loïc Mathieu
8a75fd3fe5 fix(worker): worker execution result (#2616)
* fix(core, jdbc, runner-memory): set Executable taskrun to RUNNING earlier

Or RUNNING will be set when the subflow is RUNNING and when it is terminated erasing previous RUNNING start date.

* fix(core): avoid duplicate state change in the Flow task
2023-11-30 22:33:21 +01:00
YannC
3b1bed8bf4 fix(ui): avoid admin menu blinking (#2606)
* fix(ui): avoid admin menu blinking

close #2580

* fix(ui): review changes

* fix(): not blinking with tenant
2023-11-30 22:09:21 +01:00
Loïc Mathieu
01128f0927 fix(core): possible NPE on execution.findChilds() (#2607) 2023-11-30 22:09:17 +01:00
brian.mulier
bface3755a fix(ui): left menu child is also selected when navigating in their subroutes 2023-11-30 22:08:27 +01:00
YannC
8fe518ceae fix(ui): use correct key when change display columns on execution list (#2615)
* fix(ui): use correct key when change display columns on execution list

* fix(): remove log
2023-11-30 22:08:24 +01:00
Ludovic DEHON
28e39ab756 feat(core): add Elastic Common Schema (ecs) log format dependencies
close #2611
2023-11-30 22:08:18 +01:00
Ludovic DEHON
24843cfc17 fix(core): prevent npe on subflow 2023-11-30 22:08:15 +01:00
YannC
2e1c3733a4 fix(): prerender variables (#2588)
* fix(): prerender variables

* test(): fix test

* fix(): review changes

* fix(): review changes

* fix(core): return optional of object instad of empty() when no type match
2023-11-30 22:08:09 +01:00
Loïc Mathieu
61db3ccedb chore(version): update to version 'v0.13.3' 2023-11-28 15:23:07 +01:00
YannC
6a03dc9e41 chore(version): update to version 'v0.13.2' 2023-11-24 08:50:56 +01:00
Loïc Mathieu
b2a107bc55 fix(ui): the workers API is global to all tenants 2023-11-24 08:50:40 +01:00
YannC
97a6afbe2e chore(version): update to version 'v0.13.1' 2023-11-21 19:44:48 +01:00
brian.mulier
cc800b4528 fix(ui): every unwanted things are now hidden in VSCode editor
closes #2336
closes #2338
closes #2339
2023-11-21 19:43:29 +01:00
YannC
da50cf4287 fix(): display file preview (#2569) 2023-11-21 19:42:55 +01:00
YannC
1ecfed1559 fix(): apply class correctly (#2578)
close #2576
2023-11-21 19:42:25 +01:00
brian.mulier
4a3e250019 fix(core): input streams are now properly closed to prevent exhausting connections on remote storages
closes kestra-io/storage-s3#33
2023-11-21 19:39:00 +01:00
YannC
2917c178a5 fix(): display actions buttons on topology view (#2575)
* fix(): display actions buttons on topology view

* fix(): better button position
2023-11-21 19:37:50 +01:00
YannC
0ceaeb4a97 fix(): subflow logs not working (#2552) 2023-11-21 19:36:50 +01:00
39 changed files with 443 additions and 188 deletions

View File

@@ -22,6 +22,7 @@ dependencies {
implementation group: 'net.jodah', name: 'failsafe', version: '2.4.4'
implementation 'com.github.oshi:oshi-core:6.4.6'
implementation 'io.pebbletemplates:pebble:3.2.1'
implementation group: 'co.elastic.logging', name: 'logback-ecs-encoder', version: '1.5.0'
// scheduler
implementation group: 'com.cronutils', name: 'cron-utils', version: '9.2.1'

View File

@@ -696,7 +696,7 @@ public class Execution implements DeletedInterface, TenantInterface {
* @return List of parent {@link TaskRun}
*/
public List<TaskRun> findChilds(TaskRun taskRun) {
if (taskRun.getParentTaskRunId() == null) {
if (taskRun.getParentTaskRunId() == null || this.taskRunList == null) {
return new ArrayList<>();
}

View File

@@ -1,5 +1,7 @@
package io.kestra.core.models.flows;
import com.fasterxml.jackson.databind.JsonNode;
import io.kestra.core.models.executions.Execution;
import io.micronaut.core.annotation.Introspected;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -7,6 +9,9 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.Optional;
@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@@ -15,4 +20,22 @@ import lombok.experimental.SuperBuilder;
@EqualsAndHashCode
public class FlowWithException extends FlowWithSource {
String exception;
public static Optional<FlowWithException> from(JsonNode jsonNode, Exception exception) {
if (jsonNode.hasNonNull("id") && jsonNode.hasNonNull("namespace")) {
var flow = FlowWithException.builder()
.id(jsonNode.get("id").asText())
.tenantId(jsonNode.hasNonNull("tenant_id") ? jsonNode.get("tenant_id").asText() : null)
.namespace(jsonNode.get("namespace").asText())
.revision(jsonNode.hasNonNull("revision") ? jsonNode.get("revision").asInt() : 1)
.deleted(jsonNode.hasNonNull("deleted") ? jsonNode.get("deleted").asBoolean() : false)
.exception(exception.getMessage())
.tasks(List.of())
.build();
return Optional.of(flow);
}
// if there is no id and namespace, we return null as we cannot create a meaningful FlowWithException
return Optional.empty();
}
}

View File

@@ -99,7 +99,7 @@ public final class ExecutableUtils {
return WorkerTaskExecution.builder()
.task(currentTask)
.taskRun(currentTaskRun)
.taskRun(currentTaskRun.withState(State.Type.RUNNING))
.execution(execution)
.iteration(iteration)
.build();
@@ -117,11 +117,11 @@ public final class ExecutableUtils {
State.Type currentState = taskRun.getState().getCurrent();
Optional<State.Type> previousState = taskRun.getState().getHistories().size() > 1 ? Optional.of(taskRun.getState().getHistories().get(taskRun.getState().getHistories().size() - 2).getState()) : Optional.empty();
int currentStateIteration = getIterationCounter(iterations, currentState, maxIterations) + 1;
iterations.put(currentState.toString(), currentStateIteration);
int currentStateIteration = iterations.getOrDefault(currentState.toString(), 0);
iterations.put(currentState.toString(), currentStateIteration + 1);
if (previousState.isPresent() && previousState.get() != currentState) {
int previousStateIterations = getIterationCounter(iterations, previousState.get(), maxIterations) - 1;
iterations.put(previousState.get().toString(), previousStateIterations);
int previousStateIterations = iterations.getOrDefault(previousState.get() .toString(), maxIterations);
iterations.put(previousState.get().toString(), previousStateIterations - 1);
}
// update the state to success if current == max

View File

@@ -41,7 +41,7 @@ public class Executor {
}
public Boolean canBeProcessed() {
return !(this.getException() != null || this.getFlow() == null || this.getFlow() instanceof FlowWithException || this.getExecution().isDeleted());
return !(this.getException() != null || this.getFlow() == null || this.getFlow() instanceof FlowWithException || this.getFlow().getTasks() == null || this.getExecution().isDeleted());
}
public Executor withFlow(Flow flow) {

View File

@@ -107,7 +107,6 @@ public class NamespaceFilesService {
private void copy(String tenantId, String namespace, Path basePath, List<URI> files) throws IOException {
files
.forEach(throwConsumer(f -> {
InputStream inputStream = storageInterface.get(tenantId, uri(namespace, f));
Path destination = Paths.get(basePath.toString(), f.getPath());
if (!destination.getParent().toFile().exists()) {
@@ -115,7 +114,9 @@ public class NamespaceFilesService {
destination.getParent().toFile().mkdirs();
}
Files.copy(inputStream, destination);
try (InputStream inputStream = storageInterface.get(tenantId, uri(namespace, f))) {
Files.copy(inputStream, destination);
}
}));
}
}

View File

@@ -598,7 +598,7 @@ public class RunContext {
URI uri = URI.create(this.taskStateFilePathPrefix(state, isNamespace, useTaskRun));
URI resolve = uri.resolve(uri.getPath() + "/" + name);
return this.storageInterface.get(getTenantId(), resolve);
return this.storageInterface.get(getTenantId(), resolve);
}
public URI putTaskStateFile(byte[] content, String state, String name) throws IOException {

View File

@@ -5,11 +5,6 @@ import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.HandlebarsException;
import com.github.jknack.handlebars.Template;
import com.github.jknack.handlebars.helper.*;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.error.AttributeNotFoundException;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.AbstractExtension;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.runners.handlebars.VariableRendererPlugins;
import io.kestra.core.runners.handlebars.helpers.*;
@@ -18,6 +13,15 @@ import io.kestra.core.runners.pebble.JsonWriter;
import io.kestra.core.runners.pebble.PebbleLruCache;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Nullable;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.error.AttributeNotFoundException;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.AbstractExtension;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.Getter;
import java.io.IOException;
import java.io.StringWriter;
@@ -26,11 +30,6 @@ import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.micronaut.core.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.Getter;
@Singleton
public class VariableRenderer {
private static final Pattern RAW_PATTERN = Pattern.compile("\\{%[-]*\\s*raw\\s*[-]*%\\}(.*?)\\{%[-]*\\s*endraw\\s*[-]*%\\}");
@@ -93,6 +92,10 @@ public class VariableRenderer {
}
public String recursiveRender(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return recursiveRender(inline, variables, true);
}
public String recursiveRender(String inline, Map<String, Object> variables, Boolean preprocessVariables) throws IllegalVariableEvaluationException {
if (inline == null) {
return null;
}
@@ -111,6 +114,11 @@ public class VariableRenderer {
return uuid;
});
// pre-process variables
if (preprocessVariables) {
variables = this.preprocessVariables(variables);
}
boolean isSame = false;
String current = "";
PebbleTemplate compiledTemplate;
@@ -131,28 +139,43 @@ public class VariableRenderer {
}
try {
Template template = handlebars.compileInline(currentTemplate);
Template template = handlebars.compileInline(currentTemplate);
current = template.apply(variables);
} catch (HandlebarsException | IOException hbE) {
throw new IllegalVariableEvaluationException(
"Pebble evaluation failed with '" + e.getMessage() + "' " +
"and Handlebars fallback failed also with '" + hbE.getMessage() + "'" ,
"Pebble evaluation failed with '" + e.getMessage() + "' " +
"and Handlebars fallback failed also with '" + hbE.getMessage() + "'",
e
);
}
}
isSame = currentTemplate.equals(current);
currentTemplate = current;
}
// post-process raw tags
for(var entry: replacers.entrySet()) {
for (var entry : replacers.entrySet()) {
current = current.replace(entry.getKey(), entry.getValue());
}
return current;
}
public Map<String, Object> preprocessVariables(Map<String, Object> variables) throws IllegalVariableEvaluationException {
Map<String, Object> currentVariables = variables;
Map<String, Object> previousVariables;
boolean isSame = false;
while (!isSame) {
previousVariables = currentVariables;
currentVariables = this.render(currentVariables, variables, false);
isSame = previousVariables.equals(currentVariables);
}
return currentVariables;
}
public IllegalVariableEvaluationException properPebbleException(PebbleException e) {
if (e instanceof AttributeNotFoundException current) {
return new IllegalVariableEvaluationException(
@@ -166,17 +189,27 @@ public class VariableRenderer {
return new IllegalVariableEvaluationException(e);
}
// By default, render() will render variables
// so we keep the previous behavior
public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return this.recursiveRender(inline, variables);
return this.recursiveRender(inline, variables, true);
}
public String render(String inline, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
return this.recursiveRender(inline, variables, preprocessVariables);
}
// Default behavior
public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return render(in, variables, true);
}
public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
Map<String, Object> map = new HashMap<>();
for (Map.Entry<String, Object> r : in.entrySet()) {
String key = this.render(r.getKey(), variables);
Object value = renderObject(r.getValue(), variables).orElse(r.getValue());
String key = this.render(r.getKey(), variables, preprocessVariables);
Object value = renderObject(r.getValue(), variables, preprocessVariables).orElse(r.getValue());
map.putIfAbsent(
key,
@@ -188,23 +221,25 @@ public class VariableRenderer {
}
@SuppressWarnings({"unchecked", "rawtypes"})
private Optional<Object> renderObject(Object object, Map<String, Object> variables) throws IllegalVariableEvaluationException {
private Optional<Object> renderObject(Object object, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
if (object instanceof Map) {
return Optional.of(this.render((Map) object, variables));
return Optional.of(this.render((Map) object, variables, preprocessVariables));
} else if (object instanceof Collection) {
return Optional.of(this.renderList((List) object, variables));
return Optional.of(this.renderList((List) object, variables, preprocessVariables));
} else if (object instanceof String) {
return Optional.of(this.render((String) object, variables));
return Optional.of(this.render((String) object, variables, preprocessVariables));
} else if (object == null) {
return Optional.empty();
}
return Optional.empty();
return Optional.of(object);
}
public List<Object> renderList(List<Object> list, Map<String, Object> variables) throws IllegalVariableEvaluationException {
private List<Object> renderList(List<Object> list, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
List<Object> result = new ArrayList<>();
for (Object inline : list) {
this.renderObject(inline, variables)
this.renderObject(inline, variables, preprocessVariables)
.ifPresent(result::add);
}

View File

@@ -1,7 +1,6 @@
package io.kestra.core.runners.pebble.functions;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.Slugify;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.Function;
@@ -11,6 +10,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -47,7 +47,9 @@ public class ReadFileFunction implements Function {
private String readFromNamespaceFile(EvaluationContext context, String path) throws IOException {
Map<String, String> flow = (Map<String, String>) context.getVariable("flow");
URI namespaceFile = URI.create(storageInterface.namespaceFilePrefix(flow.get("namespace")) + "/" + path);
return new String(storageInterface.get(flow.get("tenantId"), namespaceFile).readAllBytes(), StandardCharsets.UTF_8);
try (InputStream inputStream = storageInterface.get(flow.get("tenantId"), namespaceFile)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
private String readFromInternalStorageUri(EvaluationContext context, String path) throws IOException {
@@ -69,7 +71,9 @@ public class ReadFileFunction implements Function {
}
}
URI internalStorageFile = URI.create(path);
return new String(storageInterface.get(flow.get("tenantId"), internalStorageFile).readAllBytes(), StandardCharsets.UTF_8);
try (InputStream inputStream = storageInterface.get(flow.get("tenantId"), internalStorageFile)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
private boolean validateFileUri(String namespace, String flowId, String executionId, String path) {

View File

@@ -4,10 +4,7 @@ import io.kestra.core.runners.RunContext;
import io.kestra.core.storages.StorageSplitInterface;
import io.micronaut.core.convert.format.ReadableBytesTypeConverter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.io.*;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

View File

@@ -42,8 +42,7 @@ public interface StorageInterface {
* @return true if the uri points to a file/object that exist in the internal storage.
*/
default boolean exists(String tenantId, URI uri) {
try {
get(tenantId, uri);
try (InputStream ignored = get(tenantId, uri)){
return true;
} catch (IOException ieo) {
return false;

View File

@@ -175,7 +175,10 @@ public class Flow extends Task implements ExecutableTask<Flow.Output> {
taskRun = taskRun.withOutputs(builder.build().toMap());
taskRun = taskRun.withState(ExecutableUtils.guessState(execution, this.transmitFailed, State.Type.SUCCESS));
State.Type finalState = ExecutableUtils.guessState(execution, this.transmitFailed, State.Type.SUCCESS);
if (taskRun.getState().getCurrent() != finalState) {
taskRun = taskRun.withState(finalState);
}
return Optional.of(ExecutableUtils.workerTaskResult(taskRun));
}

View File

@@ -54,9 +54,9 @@ public abstract class AbstractState extends Task {
protected Map<String, Object> get(RunContext runContext) throws IllegalVariableEvaluationException, IOException {
InputStream taskStateFile = runContext.getTaskStateFile("tasks-states", runContext.render(this.name), this.namespace, this.taskrunValue);
return JacksonMapper.ofJson(false).readValue(taskStateFile, TYPE_REFERENCE);
try (InputStream taskStateFile = runContext.getTaskStateFile("tasks-states", runContext.render(this.name), this.namespace, this.taskrunValue)) {
return JacksonMapper.ofJson(false).readValue(taskStateFile, TYPE_REFERENCE);
}
}
protected Pair<URI, Map<String, Object>> merge(RunContext runContext, Map<String, Object> map) throws IllegalVariableEvaluationException, IOException {

View File

@@ -16,6 +16,7 @@ import io.kestra.core.runners.RunContext;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
@@ -128,7 +129,9 @@ public class Concat extends Task implements RunnableTask<Concat.Output> {
finalFiles.forEach(throwConsumer(s -> {
URI from = new URI(runContext.render(s));
IOUtils.copyLarge(runContext.uriToInputStream(from), fileOutputStream);
try (InputStream inputStream = runContext.uriToInputStream(from)) {
IOUtils.copyLarge(inputStream, fileOutputStream);
}
if (separator != null) {
IOUtils.copy(new ByteArrayInputStream(this.separator.getBytes()), fileOutputStream);

View File

@@ -165,7 +165,7 @@ public class FlowTopologyService {
.filter(t -> t instanceof ExecutableTask)
.map(t -> (ExecutableTask<?>) t)
.anyMatch(t ->
t.subflowId().namespace().equals(child.getNamespace()) && t.subflowId().flowId().equals(child.getId())
t.subflowId() != null && t.subflowId().namespace().equals(child.getNamespace()) && t.subflowId().flowId().equals(child.getId())
);
} catch (Exception e) {
log.warn("Failed to detect flow task on namespace:'" + parent.getNamespace() + "', flowId:'" + parent.getId() + "'", e);

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<included>
<appender name="CONSOLE_ECS_OUT" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<immediateFlush>true</immediateFlush>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>DENY</onMatch>
</filter>
<encoder class="co.elastic.logging.logback.EcsEncoder" />
</appender>
<appender name="CONSOLE_ECS_ERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<immediateFlush>true</immediateFlush>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder class="co.elastic.logging.logback.EcsEncoder" />
</appender>
</included>

View File

@@ -35,8 +35,8 @@ class FlowGraphTest extends AbstractMemoryRunnerTest {
Flow flow = this.parse("flows/valids/return.yaml");
FlowGraph flowGraph = GraphUtils.flowGraph(flow, null);
assertThat(flowGraph.getNodes().size(), is(5));
assertThat(flowGraph.getEdges().size(), is(4));
assertThat(flowGraph.getNodes().size(), is(6));
assertThat(flowGraph.getEdges().size(), is(5));
assertThat(flowGraph.getClusters().size(), is(0));
assertThat(((AbstractGraphTask) flowGraph.getNodes().get(2)).getTask().getId(), is("date"));

View File

@@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.*;
@Property(name = "kestra.tasks.tmp-dir.path", value = "/tmp/sub/dir/tmp/")
@@ -112,7 +113,7 @@ class RunContextTest extends AbstractMemoryRunnerTest {
void variables() throws TimeoutException {
Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "return");
assertThat(execution.getTaskRunList(), hasSize(3));
assertThat(execution.getTaskRunList(), hasSize(4));
assertThat(
ZonedDateTime.from(ZonedDateTime.parse((String) execution.getTaskRunList().get(0).getOutputs().get("value"))),
@@ -120,6 +121,7 @@ class RunContextTest extends AbstractMemoryRunnerTest {
);
assertThat(execution.getTaskRunList().get(1).getOutputs().get("value"), is("task-id"));
assertThat(execution.getTaskRunList().get(2).getOutputs().get("value"), is("return"));
assertThat((String) execution.getTaskRunList().get(3).getOutputs().get("value"), containsString("toto"));
}
@Test

View File

@@ -5,13 +5,13 @@ import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.runners.VariableRenderer;
import io.kestra.core.utils.Rethrow;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import jakarta.inject.Inject;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

View File

@@ -105,8 +105,8 @@ public class ForEachItemCaseTest {
assertThat(outputs.get("iterations"), notNullValue());
Map<String, Integer> iterations = (Map<String, Integer>) outputs.get("iterations");
assertThat(iterations.get("max"), is(3));
assertThat(iterations.get("CREATED"), is(0));
assertThat(iterations.get("RUNNING"), nullValue()); // if we didn't wait we will only observe CREATED and SUCCESS
assertThat(iterations.get("CREATED"), nullValue());// if we didn't wait we will only observe RUNNING and SUCCESS
assertThat(iterations.get("RUNNING"), is(0));
assertThat(iterations.get("SUCCESS"), is(3));
// assert that not all subflows ran (depending on the speed of execution there can be some)

View File

@@ -10,7 +10,7 @@ inputs:
variables:
var-1: var-1
var-2: '{{ var-1 }}'
var-2: "{{ 'var-1' }}"
taskDefaults:
- type: io.kestra.core.tasks.log.Log

View File

@@ -1,6 +1,9 @@
id: return
namespace: io.kestra.tests
variables:
period: "{{ schedule.date ?? execution.startDate }}"
tasks:
- id: date
type: io.kestra.core.tasks.debugs.Return
@@ -10,4 +13,7 @@ tasks:
format: "{{task.id}}"
- id: flow-id
type: io.kestra.core.tasks.debugs.Return
format: "{{flow.id}}"
format: "{{flow.id}}"
- id: variables
type: io.kestra.core.tasks.debugs.Return
format: "{{ vars.period | replace({':': 'toto'}) }}"

View File

@@ -1,4 +1,4 @@
version=0.13.0
version=0.13.8
jacksonVersion=2.15.2
micronautVersion=3.10.1

View File

@@ -61,14 +61,7 @@ public abstract class AbstractJdbcFlowRepository extends AbstractJdbcRepository
} catch (DeserializationException e) {
try {
JsonNode jsonNode = JdbcMapper.of().readTree(source);
return FlowWithException.builder()
.id(jsonNode.get("id").asText())
.tenantId(jsonNode.get("tenant_id") != null ? jsonNode.get("tenant_id").asText() : null)
.namespace(jsonNode.get("namespace").asText())
.revision(jsonNode.get("revision").asInt())
.exception(e.getMessage())
.tasks(List.of())
.build();
return FlowWithException.from(jsonNode, e).orElseThrow(() -> e);
} catch (JsonProcessingException ex) {
throw new DeserializationException(ex, source);
}

View File

@@ -406,7 +406,7 @@ public class JdbcExecutor implements ExecutorInterface {
// send a running worker task result to track running vs created status
if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING));
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun());
}
});
}
@@ -426,7 +426,7 @@ public class JdbcExecutor implements ExecutorInterface {
.ifPresent(workerTaskExecution -> {
// If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service.
if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING).withState(execution.getState().getCurrent()));
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(execution.getState().getCurrent()));
}
workerTaskExecutionStorage.delete(workerTaskExecution);

View File

@@ -268,7 +268,7 @@ public abstract class JdbcRunnerTest {
skipExecutionCaseTest.skipExecution();
}
@Test
@RetryingTest(5)
void forEachItem() throws URISyntaxException, IOException, InterruptedException, TimeoutException {
forEachItemCaseTest.forEachItem();
}

View File

@@ -237,7 +237,7 @@ public class MemoryExecutor implements ExecutorInterface {
// send a running worker task result to track running vs created status
if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING));
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun());
}
});
}
@@ -259,7 +259,7 @@ public class MemoryExecutor implements ExecutorInterface {
// If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service.
if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING).withState(execution.getState().getCurrent()));
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(execution.getState().getCurrent()));
}
WORKERTASKEXECUTIONS_WATCHER.remove(execution.getId());

View File

@@ -30,6 +30,31 @@ const extensionsToFetch = Object.entries(versionByExtensionIdToFetch).map(([exte
}));
// used to configure VSCode startup
const sidebarTabs = [
{"id": "workbench.view.explorer", "pinned": true, "visible": true, "order": 0},
{"id": "workbench.view.search", "pinned": true, "visible": true, "order": 1},
{"id": "workbench.view.scm", "pinned": false, "visible": false, "order": 2},
{"id": "workbench.view.debug", "pinned": false,"visible": false,"order": 3},
{"id": "workbench.view.extensions", "pinned": true,"visible": true,"order": 4},
{"id": "workbench.view.remote", "pinned": false,"visible": false,"order": 4},
{"id": "workbench.view.extension.test", "pinned": false,"visible": false,"order": 6},
{"id": "workbench.view.extension.references-view", "pinned": false,"visible": false,"order": 7},
{"id": "workbench.panel.chatSidebar", "pinned": false,"visible": false,"order": 100},
{"id": "userDataProfiles", "pinned": false, "visible": false},
{"id": "workbench.view.sync", "pinned": false,"visible": false},
{"id": "workbench.view.editSessions", "pinned": false, "visible": false}
];
const bottomBarTabs = [
{"id":"workbench.panel.markers", "pinned": false,"visible": false,"order": 0},
{"id":"workbench.panel.output", "pinned": false,"visible": false,"order": 1},
{"id":"workbench.panel.repl", "pinned": false,"visible": false,"order": 2},
{"id":"terminal", "pinned": false,"visible": false,"order": 3},
{"id":"workbench.panel.testResults", "pinned": false,"visible": false,"order": 3},
{"id":"~remote.forwardedPortsContainer", "pinned": false,"visible": false,"order": 5},
{"id":"refactorPreview", "pinned": false,"visible": false}
];
window.product = {
productConfiguration: {
nameShort: "Kestra VSCode",
@@ -77,5 +102,17 @@ window.product = {
"workbench.colorTheme": THEME === "dark" ? "Sweet Dracula" : "Default Light Modern",
// provide the Kestra root URL to extension
"kestra.api.url": KESTRA_API_URL
},
profile: {
name: "Kestra VSCode",
contents: JSON.stringify({
globalState: JSON.stringify({
"storage": {
"workbench.activity.pinnedViewlets2": sidebarTabs,
"workbench.activity.showAccounts": "false",
"workbench.panel.pinnedPanels": bottomBarTabs
}
})
})
}
};

View File

@@ -447,7 +447,7 @@
},
methods: {
onDisplayColumnsChange(event) {
localStorage.setItem("displayExecutionsColumns", event);
localStorage.setItem(this.storageKey, event);
this.displayColumns = event;
},
displayColumn(column) {
@@ -608,4 +608,4 @@
},
}
};
</script>
</script>

View File

@@ -142,7 +142,7 @@
executionId: this.executionId,
path: this.value,
maxRows: this.maxPreview,
encoding: this.encoding
encoding: this.encoding.value
})
.then(() => {
this.isPreviewOpen = true;

View File

@@ -456,15 +456,15 @@
}
.bottom-right {
bottom: var(--spacer);
right: var(--spacer);
bottom: 0px;
right: 0px;
ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
gap: calc(var(--spacer) / 2);
//gap: calc(var(--spacer) / 2);
}
}
}

View File

@@ -0,0 +1,121 @@
<template>
<div class="to-action-button">
<div v-if="isAllowedEdit || canDelete" class="mx-2">
<el-dropdown>
<el-button type="default" :disabled="isReadOnly">
<DotsVertical title=""/>
{{ $t("actions") }}
</el-button>
<template #dropdown>
<el-dropdown-menu class="m-dropdown-menu">
<el-dropdown-item
v-if="!isCreating && canDelete"
:icon="Delete"
size="large"
@click="forwardEvent('delete-flow', $event)"
>
{{ $t("delete") }}
</el-dropdown-item>
<el-dropdown-item
v-if="!isCreating"
:icon="ContentCopy"
size="large"
@click="forwardEvent('copy', $event)"
>
{{ $t("copy") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="Exclamation"
size="large"
@click="forwardEvent('open-new-error', null)"
:disabled="!flowHaveTasks"
>
{{ $t("add global error handler") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="LightningBolt"
size="large"
@click="forwardEvent('open-new-trigger', null)"
:disabled="!flowHaveTasks"
>
{{ $t("add trigger") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="FileEdit"
size="large"
@click="forwardEvent('open-edit-metadata', null)"
>
{{ $t("edit metadata") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div>
<el-button
:icon="ContentSave"
@click="forwardEvent('save', $event)"
v-if="isAllowedEdit"
:type="flowError ? 'danger' : 'primary'"
:disabled="!haveChange && !isCreating"
class="edit-flow-save-button"
>
{{ $t("save") }}
</el-button>
</div>
</div>
</template>
<script setup>
import DotsVertical from "vue-material-design-icons/DotsVertical.vue";
import Delete from "vue-material-design-icons/Delete.vue";
import ContentCopy from "vue-material-design-icons/ContentCopy.vue";
import Exclamation from "vue-material-design-icons/Exclamation.vue";
import LightningBolt from "vue-material-design-icons/LightningBolt.vue";
import FileEdit from "vue-material-design-icons/FileEdit.vue";
import ContentSave from "vue-material-design-icons/ContentSave.vue";
</script>
<script>
import {defineComponent} from "vue";
export default defineComponent({
props: {
isCreating: {
type: Boolean,
default: false
},
isReadOnly: {
type: Boolean,
default: false
},
canDelete: {
type: Boolean,
default: false
},
isAllowedEdit: {
type: Boolean,
default: false
},
haveChange: {
type: Boolean,
default: false
},
flowHaveTasks: {
type: Boolean,
default: false
},
flowError: {
type: String,
default: null
}
},
methods: {
forwardEvent(type, event) {
this.$emit(type, event);
}
}
})
</script>

View File

@@ -28,6 +28,7 @@
import {editorViewTypes} from "../../utils/constants";
import Utils from "@kestra-io/ui-libs/src/utils/Utils";
import {apiUrl} from "override/utils/route";
import EditorButtons from "./EditorButtons.vue";
const store = useStore();
const router = getCurrentInstance().appContext.config.globalProperties.$router;
@@ -675,75 +676,22 @@
<ValidationError ref="validationDomElement" tooltip-placement="bottom-start" size="small" class="ms-2" :error="flowError" :warnings="flowWarnings" />
</template>
<template #buttons>
<ul>
<li v-if="isAllowedEdit || canDelete">
<el-dropdown>
<el-button type="default" :disabled="isReadOnly">
<DotsVertical title="" />
{{ $t("actions") }}
</el-button>
<template #dropdown>
<el-dropdown-menu class="m-dropdown-menu">
<el-dropdown-item
v-if="!props.isCreating && canDelete"
:icon="Delete"
size="large"
@click="deleteFlow"
>
{{ $t("delete") }}
</el-dropdown-item>
<el-dropdown-item
v-if="!props.isCreating"
:icon="ContentCopy"
size="large"
@click="() => router.push({name: 'flows/create', query: {copy: true}})"
>
{{ $t("copy") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="Exclamation"
size="large"
@click="isNewErrorOpen = true;"
:disabled="!flowHaveTasks()"
>
{{ $t("add global error handler") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="LightningBolt"
size="large"
@click="isNewTriggerOpen = true;"
:disabled="!flowHaveTasks()"
>
{{ $t("add trigger") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="FileEdit"
size="large"
@click="isEditMetadataOpen = true;"
>
{{ $t("edit metadata") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</li>
<li>
<el-button
:icon="ContentSave"
@click="save"
v-if="isAllowedEdit"
:type="flowError ? 'danger' : 'primary'"
:disabled="!haveChange && !isCreating"
class="edit-flow-save-button"
>
{{ $t("save") }}
</el-button>
</li>
</ul>
<EditorButtons
v-if="![editorViewTypes.TOPOLOGY, editorViewTypes.SOURCE_TOPOLOGY].includes(viewType)"
:is-creating="props.isCreating"
:is-read-only="props.isReadOnly"
:can-delete="canDelete"
:is-allowed-edit="isAllowedEdit"
:have-change="haveChange"
:flow-have-tasks="flowHaveTasks()"
:flow-error="flowError"
@delete-flow="deleteFlow"
@save="save"
@copy="() => router.push({name: 'flows/create', query: {copy: true}})"
@open-new-error="isNewErrorOpen = true;"
@open-new-trigger="isNewTriggerOpen = true;"
@open-edit-metadata="isEditMetadataOpen = true;"
/>
</template>
</editor>
<div class="slider" @mousedown="dragEditor" v-if="combinedEditor" />
@@ -859,6 +807,22 @@
class="to-topology-button"
@switch-view="switchViewType"
/>
<EditorButtons
v-if="[editorViewTypes.TOPOLOGY, editorViewTypes.SOURCE_TOPOLOGY].includes(viewType)"
:is-creating="props.isCreating"
:is-read-only="props.isReadOnly"
:can-delete="canDelete"
:is-allowed-edit="isAllowedEdit"
:have-change="haveChange"
:flow-have-tasks="flowHaveTasks()"
:flow-error="flowError"
@delete-flow="deleteFlow"
@save="save"
@copy="() => router.push({name: 'flows/create', query: {copy: true}})"
@open-new-error="isNewErrorOpen = true;"
@open-new-trigger="isNewTriggerOpen = true;"
@open-edit-metadata="isEditMetadataOpen = true;"
/>
</el-card>
</template>
@@ -879,6 +843,13 @@
right: 45px;
}
.to-action-button {
position: absolute;
bottom: 30px;
right: 45px;
display: flex;
}
.editor-combined {
height: 100%;
width: 50%;

View File

@@ -53,6 +53,8 @@
:allow-auto-expand-subflows="false"
:target-execution-id="currentTaskRun.outputs.executionId"
:class="$el.classList.contains('even') ? '' : 'even'"
:show-progress-bar="showProgressBar"
:show-logs="showLogs"
/>
</DynamicScrollerItem>
</template>
@@ -315,6 +317,16 @@
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
this.showLogs
},
followExecution(executionId) {
this.$store
.dispatch("execution/followExecution", {id: executionId})
.then(sse => {
this.executionSSE = sse;
this.executionSSE.onmessage = async (event) => {
this.followedExecution = JSON.parse(event.data);
}
});
},
followLogs(executionId) {
this.$store
.dispatch("execution/followLogs", {id: executionId})

View File

@@ -1,7 +1,7 @@
<template>
<sidebar-menu
id="side-menu"
:menu="menu"
:menu="localMenu"
@update:collapsed="onToggleCollapse"
width="268px"
:collapsed="collapsed"
@@ -44,6 +44,7 @@
import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
import {mapState} from "vuex";
import AccountHardHatOutline from "vue-material-design-icons/AccountHardHatOutline.vue";
import {shallowRef} from "vue";
export default {
components: {
@@ -54,6 +55,16 @@
},
emits: ["menu-collapse"],
methods: {
flattenMenu(menu) {
return menu.reduce((acc, item) => {
if (item.child) {
acc.push(...this.flattenMenu(item.child));
}
acc.push(item);
return acc;
}, []);
},
onToggleCollapse(folded) {
this.collapsed = folded;
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
@@ -71,7 +82,7 @@
r.class = "vsm--link_active";
}
if (r.child && r.child.some(c => this.$route.path.startsWith(c.href))) {
if (r.child && r.child.some(c => this.$route.path.startsWith(c.href) || c.routes?.includes(this.$route.name))) {
r.class = "vsm--link_active";
r.child = this.disabledCurrentRoute(r.child);
}
@@ -88,7 +99,7 @@
href: {name: "home"},
title: this.$t("homeDashboard.title"),
icon: {
element: ViewDashboardVariantOutline,
element: shallowRef(ViewDashboardVariantOutline),
class: "menu-icon",
},
},
@@ -96,7 +107,7 @@
href: {name: "editor"},
title: this.$t("editor"),
icon: {
element: FolderEditOutline,
element: shallowRef(FolderEditOutline),
class: "menu-icon",
},
},
@@ -105,7 +116,7 @@
routes: this.routeStartWith("flows"),
title: this.$t("flows"),
icon: {
element: FileTreeOutline,
element: shallowRef(FileTreeOutline),
class: "menu-icon",
},
exact: false,
@@ -115,7 +126,7 @@
routes: this.routeStartWith("templates"),
title: this.$t("templates"),
icon: {
element: ContentCopy,
element: shallowRef(ContentCopy),
class: "menu-icon",
},
hidden: !this.configs.isTemplateEnabled
@@ -125,7 +136,7 @@
routes: this.routeStartWith("executions"),
title: this.$t("executions"),
icon: {
element: TimelineClockOutline,
element: shallowRef(TimelineClockOutline),
class: "menu-icon"
},
},
@@ -134,7 +145,7 @@
routes: this.routeStartWith("taskruns"),
title: this.$t("taskruns"),
icon: {
element: TimelineTextOutline,
element: shallowRef(TimelineTextOutline),
class: "menu-icon"
},
hidden: !this.configs.isTaskRunEnabled
@@ -144,7 +155,7 @@
routes: this.routeStartWith("logs"),
title: this.$t("logs"),
icon: {
element: NotebookOutline,
element: shallowRef(NotebookOutline),
class: "menu-icon"
},
},
@@ -153,7 +164,7 @@
routes: this.routeStartWith("blueprints"),
title: this.$t("blueprints.title"),
icon: {
element: Ballot,
element: shallowRef(Ballot),
class: "menu-icon"
},
},
@@ -161,7 +172,7 @@
title: this.$t("administration"),
routes: this.routeStartWith("admin"),
icon: {
element: AccountSupervisorOutline,
element: shallowRef(AccountSupervisorOutline),
class: "menu-icon"
},
child: [
@@ -170,7 +181,7 @@
routes: this.routeStartWith("admin/triggers"),
title: this.$t("triggers"),
icon: {
element: TimerCogOutline,
element: shallowRef(TimerCogOutline),
class: "menu-icon"
}
},
@@ -179,7 +190,7 @@
routes: this.routeStartWith("admin/workers"),
title: this.$t("workers"),
icon: {
element: AccountHardHatOutline,
element: shallowRef(AccountHardHatOutline),
class: "menu-icon"
},
}
@@ -190,41 +201,54 @@
routes: this.routeStartWith("admin/settings"),
title: this.$t("settings"),
icon: {
element: CogOutline,
element: shallowRef(CogOutline),
class: "menu-icon"
}
}
];
},
expandParentIfNeeded() {
document.querySelectorAll(".vsm--link_level-1.vsm--link_active:not(.vsm--link_open)[aria-haspopup]").forEach(e => e.click());
document.querySelectorAll(".vsm--link.vsm--link_level-1.vsm--link_active:not(.vsm--link_open)[aria-haspopup]").forEach(e => {
e.click()
});
}
},
updated() {
// Required here because in mounted() the menu is not yet rendered
this.expandParentIfNeeded();
},
watch: {
menu: {
handler() {
this.$el.querySelectorAll(".vsm--item span").forEach(e => {
//empty icon name on mouseover
e.setAttribute("title", "")
});
this.expandParentIfNeeded();
handler(newVal, oldVal) {
// Check if the active menu item has changed, if yes then update the menu
if (JSON.stringify(this.flattenMenu(newVal).map(e => e.class?.includes("vsm--link_active") ?? false)) !==
JSON.stringify(this.flattenMenu(oldVal).map(e => e.class?.includes("vsm--link_active") ?? false))) {
this.localMenu = newVal;
this.$el.querySelectorAll(".vsm--item span").forEach(e => {
//empty icon name on mouseover
e.setAttribute("title", "")
});
}
},
flush: 'post'
}
flush: "post",
deep: true
},
},
data() {
return {
collapsed: localStorage.getItem("menuCollapsed") === "true",
localMenu: []
};
},
computed: {
...mapState("misc", ["configs"]),
...
mapState("misc", ["configs"]),
menu() {
return this.disabledCurrentRoute(this.generateMenu());
}
},
mounted() {
this.expandParentIfNeeded();
this.localMenu = this.menu;
}
};
</script>

View File

@@ -1,11 +1,11 @@
import {apiUrl} from "override/utils/route";
import {apiUrlWithoutTenants} from "override/utils/route";
export default {
namespaced: true,
actions: {
findAll(_, __) {
return this.$http.get(`${apiUrl(this)}/workers`).then(response => {
return this.$http.get(`${apiUrlWithoutTenants(this)}/workers`).then(response => {
return response.data;
})
}

View File

@@ -413,11 +413,6 @@ form.ks-horizontal {
&.is-dark {
color: var(--bs-gray-100);
html:not(.dark) & {
* {
color: var(--bs-gray-100);
}
}
background: var(--bs-gray-900);
border: 1px solid var(--bs-border-color);

View File

@@ -1009,12 +1009,11 @@ public class ExecutionController {
@Parameter(description = "The execution id") @PathVariable String executionId,
@Parameter(description = "The internal storage uri") @QueryValue URI path,
@Parameter(description = "The max row returns") @QueryValue @Nullable Integer maxRows,
@Parameter(description = "The file encoding as Java charset name. Defaults to UTF-8", example = "ISO-8859-1") @QueryValue @Nullable String encoding
@Parameter(description = "The file encoding as Java charset name. Defaults to UTF-8", example = "ISO-8859-1") @QueryValue(defaultValue = "UTF-8") String encoding
) throws IOException {
this.validateFile(executionId, path, "/api/v1/executions/{executionId}/file?path=" + path);
String extension = FilenameUtils.getExtension(path.toString());
InputStream fileStream = storageInterface.get(tenantService.resolveTenant(), path);
Optional<Charset> charset;
try {
@@ -1023,13 +1022,15 @@ public class ExecutionController {
throw new IllegalArgumentException("Unable to preview using encoding '" + encoding + "'");
}
FileRender fileRender = FileRenderBuilder.of(
extension,
fileStream,
charset,
maxRows == null ? this.initialPreviewRows : (maxRows > this.maxPreviewRows ? this.maxPreviewRows : maxRows)
);
try (InputStream fileStream = storageInterface.get(tenantService.resolveTenant(), path)){
FileRender fileRender = FileRenderBuilder.of(
extension,
fileStream,
charset,
maxRows == null ? this.initialPreviewRows : (maxRows > this.maxPreviewRows ? this.maxPreviewRows : maxRows)
);
return HttpResponse.ok(fileRender);
return HttpResponse.ok(fileRender);
}
}
}

View File

@@ -138,7 +138,9 @@ public class NamespaceFileController {
) throws IOException, URISyntaxException {
ensureWritableFile(path);
storageInterface.put(tenantService.resolveTenant(), toNamespacedStorageUri(namespace, path), new BufferedInputStream(fileContent.getInputStream()));
try(BufferedInputStream inputStream = new BufferedInputStream(fileContent.getInputStream())) {
storageInterface.put(tenantService.resolveTenant(), toNamespacedStorageUri(namespace, path), inputStream);
}
}
@ExecuteOn(TaskExecutors.IO)