Compare commits

...

17 Commits

Author SHA1 Message Date
YannC
c757827b9d chore: upgrade to version 0.16.2 2024-04-22 18:49:39 +02:00
YannC
2a5c82b2a3 fix(scheduler): better handling of locked triggers (#3603) 2024-04-22 18:47:17 +02:00
Loïc Mathieu
15bb0ee65b fea(core): mandate that both key and value are present for labels 2024-04-22 18:47:05 +02:00
Ludovic DEHON
d06e8dad6e fix(core): handle secret in trigger 2024-04-22 18:46:57 +02:00
brian.mulier
e9f5752278 fix(cli): API commands work against a pre-micronaut-upgrade server 2024-04-22 18:46:50 +02:00
brian.mulier
c16c5ddaf5 chore(deps): update ui-libs to 0.0.43 2024-04-22 18:46:44 +02:00
Ludovic DEHON
b706ca1911 fix(ui): flow full revision is truncated
close #3478
2024-04-22 18:46:36 +02:00
brian.mulier
366246e0a8 fix(ui): Gantt clicks are working again 2024-04-22 18:46:28 +02:00
brian.mulier
dcea4551cc fix(ui): prevent editor shrink on loading task runner doc 2024-04-22 18:46:22 +02:00
brian.mulier
c0ff6fcc52 fix(tests): add real launch to outputDirDisabled test for task runners 2024-04-22 18:46:16 +02:00
Florian Hussonnois
0525e7eaca fix(core): VariableRenderer should expose alternativeRender 2024-04-22 18:46:06 +02:00
YannC
d1fb098f5b chore(version): update to version 'v0.16.1' 2024-04-15 17:04:32 +02:00
YannC
9703cc48cb feat(ui): click anywhere on the row to open logs of a task in Gantt vue 2024-04-15 17:02:18 +02:00
YannC
31c3e5a4f6 feat(ui): set plugins menu back in the UI (#3558) 2024-04-15 17:02:13 +02:00
brian.mulier
bda52eb49d fix(ui): use new Monaco API for decorations to prevent editor from disappearing
closes #3536
2024-04-15 17:02:00 +02:00
brian.mulier
8a54b8ec7f fix(validate): restore ability to run validate command without any configuration 2024-04-15 17:01:34 +02:00
Loïc Mathieu
c34c82c1f9 fix: downgrade Micronaut
Go back to previously working version 4.3.4 as 4.3.7 have a bug when randomly the routeMatch is null on the security filter.
See https://github.com/kestra-io/kestra-ee/issues/1085
2024-04-15 17:01:24 +02:00
25 changed files with 265 additions and 56 deletions

View File

@@ -197,6 +197,7 @@ subprojects {
environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n" environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n"
environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString() environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString()
environment 'SECRET_NON_B64_SECRET', "some secret value" environment 'SECRET_NON_B64_SECRET', "some secret value"
environment 'SECRET_PASSWORD', "cGFzc3dvcmQ="
environment 'KESTRA_TEST1', "true" environment 'KESTRA_TEST1', "true"
environment 'KESTRA_TEST2', "Pass by env" environment 'KESTRA_TEST2', "Pass by env"
} }

View File

@@ -3,10 +3,15 @@ package io.kestra.cli;
import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.body.ContextlessMessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.client.DefaultHttpClientConfiguration; import io.micronaut.http.client.DefaultHttpClientConfiguration;
import io.micronaut.http.client.HttpClientConfiguration; import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.netty.DefaultHttpClient; import io.micronaut.http.client.netty.DefaultHttpClient;
import io.micronaut.http.netty.body.NettyJsonHandler;
import io.micronaut.json.JsonMapper;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import picocli.CommandLine; import picocli.CommandLine;
@@ -39,7 +44,12 @@ public abstract class AbstractApiCommand extends AbstractCommand {
private HttpClientConfiguration httpClientConfiguration; private HttpClientConfiguration httpClientConfiguration;
protected DefaultHttpClient client() throws URISyntaxException { protected DefaultHttpClient client() throws URISyntaxException {
return new DefaultHttpClient(server.toURI(), httpClientConfiguration != null ? httpClientConfiguration : new DefaultHttpClientConfiguration()); DefaultHttpClient defaultHttpClient = new DefaultHttpClient(server.toURI(), httpClientConfiguration != null ? httpClientConfiguration : new DefaultHttpClientConfiguration());
MessageBodyHandlerRegistry defaultHandlerRegistry = defaultHttpClient.getHandlerRegistry();
if (defaultHandlerRegistry instanceof ContextlessMessageBodyHandlerRegistry modifiableRegistry) {
modifiableRegistry.add(MediaType.TEXT_JSON_TYPE, new NettyJsonHandler<>(JsonMapper.createDefault()));
}
return defaultHttpClient;
} }
protected <T> HttpRequest<T> requestOptions(MutableHttpRequest<T> request) { protected <T> HttpRequest<T> requestOptions(MutableHttpRequest<T> request) {

View File

@@ -19,7 +19,7 @@ class FlowValidateCommandTest {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out)); System.setOut(new PrintStream(out));
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) { try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class); EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
embeddedServer.start(); embeddedServer.start();
@@ -39,7 +39,7 @@ class FlowValidateCommandTest {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out)); System.setOut(new PrintStream(out));
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) { try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class); EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
embeddedServer.start(); embeddedServer.start();

View File

@@ -532,6 +532,10 @@ public class RunContext {
this.initBean(applicationContext); this.initBean(applicationContext);
this.initLogger(workerTrigger.getTriggerContext(), workerTrigger.getTrigger()); this.initLogger(workerTrigger.getTriggerContext(), workerTrigger.getTrigger());
Map<String, Object> clone = new HashMap<>(this.variables);
clone.put("addSecretConsumer", (Consumer<String>) s -> runContextLogger.usedSecret(s));
this.variables = ImmutableMap.copyOf(clone);
// Mutability hack to update the triggerExecutionId for each evaluation on the worker // Mutability hack to update the triggerExecutionId for each evaluation on the worker
return forScheduler(workerTrigger.getTriggerContext(), workerTrigger.getTrigger()); return forScheduler(workerTrigger.getTriggerContext(), workerTrigger.getTrigger());
} }

View File

@@ -99,10 +99,16 @@ public class VariableRenderer {
Writer writer = new JsonWriter(new StringWriter()); Writer writer = new JsonWriter(new StringWriter());
compiledTemplate.evaluate(writer, variables); compiledTemplate.evaluate(writer, variables);
result = writer.toString(); result = writer.toString();
} catch (IOException e) { } catch (IOException | PebbleException e) {
throw new IllegalVariableEvaluationException(e); String alternativeRender = this.alternativeRender(e, inline, variables);
} catch (PebbleException e) { if (alternativeRender == null) {
throw properPebbleException(e); if (e instanceof PebbleException) {
throw properPebbleException((PebbleException) e);
}
throw new IllegalVariableEvaluationException(e);
} else {
result = alternativeRender;
}
} }
// post-process raw tags // post-process raw tags
@@ -111,6 +117,18 @@ public class VariableRenderer {
return result; return result;
} }
/**
* This method can be used in fallback for rendering an input string.
*
* @param e The exception that was throw by the default variable renderer.
* @param inline The expression to be rendered.
* @param variables The context variables.
* @return The rendered string.
*/
protected String alternativeRender(Exception e, String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return null;
}
private static String putBackRawTags(Map<String, String> replacers, String result) { private static String putBackRawTags(Map<String, String> replacers, String result) {
for (var entry : replacers.entrySet()) { for (var entry : replacers.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue()); result = result.replace(entry.getKey(), entry.getValue());

View File

@@ -352,6 +352,11 @@ public abstract class AbstractScheduler implements Scheduler, Service {
.conditionContext(flowWithTriggers.getConditionContext()) .conditionContext(flowWithTriggers.getConditionContext())
.triggerContext(flowWithTriggers.TriggerContext.toBuilder().date(now()).stopAfter(flowWithTriggers.getAbstractTrigger().getStopAfter()).build()) .triggerContext(flowWithTriggers.TriggerContext.toBuilder().date(now()).stopAfter(flowWithTriggers.getAbstractTrigger().getStopAfter()).build())
.build()) .build())
.peek(f -> {
if (f.getTriggerContext().getEvaluateRunningDate() != null || isExecutionNotRunning(f)) {
this.triggerState.unlock(f.getTriggerContext());
}
})
.filter(f -> f.getTriggerContext().getEvaluateRunningDate() == null) .filter(f -> f.getTriggerContext().getEvaluateRunningDate() == null)
.filter(this::isExecutionNotRunning) .filter(this::isExecutionNotRunning)
.map(FlowWithPollingTriggerNextDate::of) .map(FlowWithPollingTriggerNextDate::of)

View File

@@ -26,6 +26,13 @@ public interface SchedulerTriggerStateInterface {
List<Trigger> findByNextExecutionDateReadyForAllTenants(ZonedDateTime now, ScheduleContextInterface scheduleContext); List<Trigger> findByNextExecutionDateReadyForAllTenants(ZonedDateTime now, ScheduleContextInterface scheduleContext);
// Required for Kafka /**
* Required for Kafka
*/
List<Trigger> findByNextExecutionDateReadyForGivenFlows(List<Flow> flows, ZonedDateTime now, ScheduleContextInterface scheduleContext); List<Trigger> findByNextExecutionDateReadyForGivenFlows(List<Flow> flows, ZonedDateTime now, ScheduleContextInterface scheduleContext);
/**
* Required for Kafka
*/
void unlock(Trigger trigger);
} }

View File

@@ -36,6 +36,8 @@ import java.util.stream.StreamSupport;
@Singleton @Singleton
@Slf4j @Slf4j
public class FlowService { public class FlowService {
private final IllegalStateException NO_REPOSITORY_EXCEPTION = new IllegalStateException("No flow repository found. Make sure the `kestra.repository.type` property is set.");
@Inject @Inject
RunContextFactory runContextFactory; RunContextFactory runContextFactory;
@@ -43,7 +45,7 @@ public class FlowService {
ConditionService conditionService; ConditionService conditionService;
@Inject @Inject
FlowRepositoryInterface flowRepository; Optional<FlowRepositoryInterface> flowRepository;
@Inject @Inject
YamlFlowParser yamlFlowParser; YamlFlowParser yamlFlowParser;
@@ -62,6 +64,11 @@ public class FlowService {
.tenantId(tenantId) .tenantId(tenantId)
.build(); .build();
if (flowRepository.isEmpty()) {
throw NO_REPOSITORY_EXCEPTION;
}
FlowRepositoryInterface flowRepository = this.flowRepository.get();
return flowRepository return flowRepository
.findById(withTenant.getTenantId(), withTenant.getNamespace(), withTenant.getId()) .findById(withTenant.getTenantId(), withTenant.getNamespace(), withTenant.getId())
.map(previous -> flowRepository.update(withTenant, previous, source, taskDefaultService.injectDefaults(withTenant))) .map(previous -> flowRepository.update(withTenant, previous, source, taskDefaultService.injectDefaults(withTenant)))
@@ -69,7 +76,11 @@ public class FlowService {
} }
public List<FlowWithSource> findByNamespaceWithSource(String tenantId, String namespace) { public List<FlowWithSource> findByNamespaceWithSource(String tenantId, String namespace) {
return flowRepository.findByNamespaceWithSource(tenantId, namespace); if (flowRepository.isEmpty()) {
throw NO_REPOSITORY_EXCEPTION;
}
return flowRepository.get().findByNamespaceWithSource(tenantId, namespace);
} }
public Stream<Flow> keepLastVersion(Stream<Flow> stream) { public Stream<Flow> keepLastVersion(Stream<Flow> stream) {

View File

@@ -34,6 +34,7 @@ public class TaskDefaultService {
@Inject @Inject
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED) @Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
@Nullable
protected QueueInterface<LogEntry> logQueue; protected QueueInterface<LogEntry> logQueue;
/** /**

View File

@@ -51,10 +51,15 @@ public abstract class AbstractTaskRunnerTest {
var commands = initScriptCommands(runContext); var commands = initScriptCommands(runContext);
Mockito.when(commands.getEnableOutputDirectory()).thenReturn(false); Mockito.when(commands.getEnableOutputDirectory()).thenReturn(false);
Mockito.when(commands.outputDirectoryEnabled()).thenReturn(false); Mockito.when(commands.outputDirectoryEnabled()).thenReturn(false);
Mockito.when(commands.getCommands()).thenReturn(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("echo 'Hello World'")));
var taskRunner = taskRunner(); var taskRunner = taskRunner();
assertThat(taskRunner.additionalVars(runContext, commands).containsKey(ScriptService.VAR_OUTPUT_DIR), is(false)); assertThat(taskRunner.additionalVars(runContext, commands).containsKey(ScriptService.VAR_OUTPUT_DIR), is(false));
assertThat(taskRunner.env(runContext, commands).containsKey(ScriptService.ENV_OUTPUT_DIR), is(false)); assertThat(taskRunner.env(runContext, commands).containsKey(ScriptService.ENV_OUTPUT_DIR), is(false));
var result = taskRunner.run(runContext, commands, Collections.emptyList(), Collections.emptyList());
assertThat(result, notNullValue());
assertThat(result.getExitCode(), is(0));
} }
@Test @Test

View File

@@ -5,8 +5,6 @@ import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.flows.Flow; import io.kestra.core.models.flows.Flow;
import io.kestra.core.queues.QueueFactoryInterface; import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface; import io.kestra.core.queues.QueueInterface;
import io.kestra.core.tasks.log.Log;
import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.TestsUtils; import io.kestra.core.utils.TestsUtils;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@@ -40,7 +38,6 @@ class RunContextLoggerTest {
Flow flow = TestsUtils.mockFlow(); Flow flow = TestsUtils.mockFlow();
Execution execution = TestsUtils.mockExecution(flow, Map.of()); Execution execution = TestsUtils.mockExecution(flow, Map.of());
Log log = Log.builder().id(IdUtils.create()).type(Log.class.getName()).build();
RunContextLogger runContextLogger = new RunContextLogger( RunContextLogger runContextLogger = new RunContextLogger(
logQueue, logQueue,
@@ -86,14 +83,13 @@ class RunContextLoggerTest {
} }
@Test @Test
void secrets() throws InterruptedException { void secrets() {
List<LogEntry> logs = new CopyOnWriteArrayList<>(); List<LogEntry> logs = new CopyOnWriteArrayList<>();
List<LogEntry> matchingLog; List<LogEntry> matchingLog;
logQueue.receive(either -> logs.add(either.getLeft())); logQueue.receive(either -> logs.add(either.getLeft()));
Flow flow = TestsUtils.mockFlow(); Flow flow = TestsUtils.mockFlow();
Execution execution = TestsUtils.mockExecution(flow, Map.of()); Execution execution = TestsUtils.mockExecution(flow, Map.of());
Log log = Log.builder().id(IdUtils.create()).type(Log.class.getName()).build();
RunContextLogger runContextLogger = new RunContextLogger( RunContextLogger runContextLogger = new RunContextLogger(
logQueue, logQueue,

View File

@@ -4,6 +4,8 @@ import io.kestra.core.encryption.EncryptionService;
import io.kestra.core.exceptions.IllegalVariableEvaluationException; import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.metrics.MetricRegistry; import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.Label; import io.kestra.core.models.Label;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.conditions.ConditionContext;
import io.kestra.core.models.executions.Execution; import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.LogEntry; import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.executions.TaskRun; import io.kestra.core.models.executions.TaskRun;
@@ -14,16 +16,27 @@ import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.Type; import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.StringInput; import io.kestra.core.models.flows.input.StringInput;
import io.kestra.core.models.tasks.common.EncryptedString; import io.kestra.core.models.tasks.common.EncryptedString;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.triggers.PollingTriggerInterface;
import io.kestra.core.models.triggers.TriggerContext;
import io.kestra.core.queues.QueueFactoryInterface; import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface; import io.kestra.core.queues.QueueInterface;
import io.kestra.core.storages.StorageInterface; import io.kestra.core.storages.StorageInterface;
import io.kestra.core.tasks.test.PollingTrigger; import io.kestra.core.tasks.test.PollingTrigger;
import io.kestra.core.tasks.test.SleepTrigger;
import io.kestra.core.utils.IdUtils; import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.TestsUtils; import io.kestra.core.utils.TestsUtils;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Value; import io.micronaut.context.annotation.Value;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.exparity.hamcrest.date.ZonedDateTimeMatchers; import org.exparity.hamcrest.date.ZonedDateTimeMatchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.slf4j.event.Level; import org.slf4j.event.Level;
@@ -47,6 +60,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
@Property(name = "kestra.tasks.tmp-dir.path", value = "/tmp/sub/dir/tmp/") @Property(name = "kestra.tasks.tmp-dir.path", value = "/tmp/sub/dir/tmp/")
class RunContextTest extends AbstractMemoryRunnerTest { class RunContextTest extends AbstractMemoryRunnerTest {
@Inject
ApplicationContext applicationContext;
@Inject @Inject
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED) @Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
QueueInterface<LogEntry> workerTaskLogQueue; QueueInterface<LogEntry> workerTaskLogQueue;
@@ -66,6 +82,10 @@ class RunContextTest extends AbstractMemoryRunnerTest {
@Value("${kestra.encryption.secret-key}") @Value("${kestra.encryption.secret-key}")
private String secretKey; private String secretKey;
@Inject
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
private QueueInterface<LogEntry> logQueue;
@Test @Test
void logs() throws TimeoutException { void logs() throws TimeoutException {
List<LogEntry> logs = new CopyOnWriteArrayList<>(); List<LogEntry> logs = new CopyOnWriteArrayList<>();
@@ -289,4 +309,57 @@ class RunContextTest extends AbstractMemoryRunnerTest {
)); ));
assertThat(rendered.get("key"), is("value")); assertThat(rendered.get("key"), is("value"));
} }
@Test
void secretTrigger() throws IllegalVariableEvaluationException {
List<LogEntry> logs = new CopyOnWriteArrayList<>();
List<LogEntry> matchingLog;
logQueue.receive(either -> logs.add(either.getLeft()));
LogTrigger trigger = LogTrigger.builder()
.type(SleepTrigger.class.getName())
.id("unit-test")
.format("john {{ secret('PASSWORD') }} doe")
.build();
Map.Entry<ConditionContext, TriggerContext> mockedTrigger = TestsUtils.mockTrigger(runContextFactory, trigger);
WorkerTrigger workerTrigger = WorkerTrigger.builder()
.trigger(trigger)
.triggerContext(mockedTrigger.getValue())
.conditionContext(mockedTrigger.getKey())
.build();
RunContext runContext = mockedTrigger.getKey().getRunContext().forWorker(applicationContext, workerTrigger);
Optional<Execution> evaluate = trigger.evaluate(mockedTrigger.getKey().withRunContext(runContext), mockedTrigger.getValue());
matchingLog = TestsUtils.awaitLogs(logs, 3);
assertThat(matchingLog.stream().filter(logEntry -> logEntry.getLevel().equals(Level.INFO)).findFirst().orElse(null).getMessage(), is("john ******** doe"));
}
@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
public static class LogTrigger extends AbstractTrigger implements PollingTriggerInterface {
@PluginProperty
@NotNull
private String format;
@Override
public Optional<Execution> evaluate(ConditionContext conditionContext, TriggerContext context) throws IllegalVariableEvaluationException {
conditionContext.getRunContext().logger().info(conditionContext.getRunContext().render(format));
return Optional.empty();
}
@Override
public Duration getInterval() {
return null;
}
}
} }

View File

@@ -0,0 +1,43 @@
package io.kestra.core.runners;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.micronaut.context.ApplicationContext;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Map;
@MicronautTest
class VariableRendererTest {
@Inject
ApplicationContext applicationContext;
@Inject
VariableRenderer.VariableConfiguration variableConfiguration;
@Test
void shouldRenderUsingAlternativeRendering() throws IllegalVariableEvaluationException {
TestVariableRenderer renderer = new TestVariableRenderer(applicationContext, variableConfiguration);
String render = renderer.render("{{ dummy }}", Map.of());
Assertions.assertEquals("result", render);
}
public static class TestVariableRenderer extends VariableRenderer {
public TestVariableRenderer(ApplicationContext applicationContext,
VariableConfiguration variableConfiguration) {
super(applicationContext, variableConfiguration);
}
@Override
protected String alternativeRender(Exception e, String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return "result";
}
}
}

View File

@@ -1,7 +1,8 @@
version=0.16.0 version=0.16.2
jacksonVersion=2.16.2 jacksonVersion=2.16.2
micronautVersion=4.3.7 # Cannot upgrade for now due to https://github.com/kestra-io/kestra-ee/issues/1085
micronautVersion=4.3.4
lombokVersion=1.18.32 lombokVersion=1.18.32
slf4jVersion=2.0.12 slf4jVersion=2.0.12

View File

@@ -84,4 +84,7 @@ public class JdbcSchedulerTriggerState implements SchedulerTriggerStateInterface
public List<Trigger> findByNextExecutionDateReadyForGivenFlows(List<Flow> flows, ZonedDateTime now, ScheduleContextInterface scheduleContext) { public List<Trigger> findByNextExecutionDateReadyForGivenFlows(List<Flow> flows, ZonedDateTime now, ScheduleContextInterface scheduleContext) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@Override
public void unlock(Trigger trigger) {}
} }

View File

@@ -79,4 +79,7 @@ public class MemorySchedulerTriggerState implements SchedulerTriggerStateInterfa
public List<Trigger> findByNextExecutionDateReadyForGivenFlows(List<Flow> flows, ZonedDateTime now, ScheduleContextInterface scheduleContext) { public List<Trigger> findByNextExecutionDateReadyForGivenFlows(List<Flow> flows, ZonedDateTime now, ScheduleContextInterface scheduleContext) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@Override
public void unlock(Trigger trigger) {}
} }

8
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "kestra", "name": "kestra",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@kestra-io/ui-libs": "^0.0.42", "@kestra-io/ui-libs": "^0.0.43",
"@vue-flow/background": "^1.3.0", "@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1", "@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5", "@vue-flow/core": "^1.33.5",
@@ -704,9 +704,9 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
}, },
"node_modules/@kestra-io/ui-libs": { "node_modules/@kestra-io/ui-libs": {
"version": "0.0.42", "version": "0.0.43",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.42.tgz", "resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.43.tgz",
"integrity": "sha512-3Uti8oK94KDNcxxxpODdWk7Ed1BUzGNdKOrJXMt0IfYqrCLJ3bh11C92oBJyS4LN5PHxZjLsAFay/akiGNavtA==", "integrity": "sha512-tDGJD+yTn3L5e+c64hJBYb3qOzpw0y3OZML8nXb3trZ5/q0s1TkruY1twu5MrJxhZzNXUr0EA/VlxEQZ2ZKVCQ==",
"peerDependencies": { "peerDependencies": {
"@vue-flow/background": "^1.3.0", "@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1", "@vue-flow/controls": "^1.1.1",

View File

@@ -12,7 +12,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
}, },
"dependencies": { "dependencies": {
"@kestra-io/ui-libs": "^0.0.42", "@kestra-io/ui-libs": "^0.0.43",
"@vue-flow/background": "^1.3.0", "@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1", "@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5", "@vue-flow/core": "^1.33.5",

View File

@@ -25,7 +25,7 @@
</span> </span>
</el-tooltip> </el-tooltip>
</th> </th>
<td :colspan="dates.length"> <td :colspan="dates.length" @click="onTaskSelect(serie.task)" class="cursor-pointer">
<el-tooltip placement="top" :persistent="false" transition="" :hide-after="0"> <el-tooltip placement="top" :persistent="false" transition="" :hide-after="0">
<template #content> <template #content>
<span style="white-space: pre-wrap;"> <span style="white-space: pre-wrap;">
@@ -35,7 +35,7 @@
<div <div
:style="{left: serie.start + '%', width: serie.width + '%'}" :style="{left: serie.start + '%', width: serie.width + '%'}"
class="task-progress" class="task-progress"
@click="onTaskSelect(serie.task)" @click="onTaskSelect(serie.id)"
> >
<div class="progress"> <div class="progress">
<div <div
@@ -58,7 +58,7 @@
@follow="forwardEvent('follow', $event)" @follow="forwardEvent('follow', $event)"
:target-execution="execution" :target-execution="execution"
:target-flow="flow" :target-flow="flow"
:show-logs="taskTypeByTaskRunId[serie.task.id] !== 'io.kestra.core.tasks.flows.ForEachItem'" :show-logs="taskTypeByTaskRunId[serie.id] !== 'io.kestra.core.tasks.flows.ForEachItem'"
/> />
</td> </td>
</tr> </tr>
@@ -294,13 +294,13 @@
} }
this.dates = dates; this.dates = dates;
}, },
onTaskSelect(taskRun) { onTaskSelect(taskRunId) {
if(this.selectedTaskRuns.includes(taskRun.id)) { if(this.selectedTaskRuns.includes(taskRunId)) {
this.selectedTaskRuns = this.selectedTaskRuns.filter(id => id !== taskRun.id); this.selectedTaskRuns = this.selectedTaskRuns.filter(id => id !== taskRunId);
return return
} }
this.selectedTaskRuns.push(taskRun.id); this.selectedTaskRuns.push(taskRunId);
}, },
stopRealTime() { stopRealTime() {
this.realTime = false this.realTime = false
@@ -323,6 +323,10 @@
} }
.cursor-pointer {
cursor: pointer;
}
table { table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;

View File

@@ -79,7 +79,7 @@
<h5>{{ $t("revision") + `: ` + revision }}</h5> <h5>{{ $t("revision") + `: ` + revision }}</h5>
</template> </template>
<editor v-model="revisionYaml" lang="yaml" /> <editor v-model="revisionYaml" lang="yaml" :full-height="false" :input="true" :navbar="false" :read-only="true" />
</drawer> </drawer>
</div> </div>
<div v-else> <div v-else>

View File

@@ -102,7 +102,6 @@
BookMultipleOutline: shallowRef(BookMultipleOutline), BookMultipleOutline: shallowRef(BookMultipleOutline),
Close: shallowRef(Close) Close: shallowRef(Close)
}, },
oldDecorations: [],
editorDocumentation: undefined, editorDocumentation: undefined,
plugin: undefined, plugin: undefined,
taskType: undefined, taskType: undefined,
@@ -209,6 +208,8 @@
this.editor = editor; this.editor = editor;
this.decorations = this.editor.createDecorationsCollection();
if (!this.original) { if (!this.original) {
this.editor.onDidBlurEditorWidget(() => { this.editor.onDidBlurEditorWidget(() => {
this.$emit("focusout", editor.getValue()); this.$emit("focusout", editor.getValue());
@@ -308,27 +309,27 @@
this.editor.onDidContentSizeChange(_ => { this.editor.onDidContentSizeChange(_ => {
if (this.guidedProperties.monacoRange) { if (this.guidedProperties.monacoRange) {
editor.revealLine(this.guidedProperties.monacoRange.endLineNumber); editor.revealLine(this.guidedProperties.monacoRange.endLineNumber);
let decorations = [ const decorationsToAdd = [];
{ decorationsToAdd.push({
range: this.guidedProperties.monacoRange, range: this.guidedProperties.monacoRange,
options: { options: {
isWholeLine: true, isWholeLine: true,
inlineClassName: "highlight-text" inlineClassName: "highlight-text"
}, },
className: "highlight-text", className: "highlight-text",
} });
]; if (this.guidedProperties.monacoDisableRange) {
decorations = this.guidedProperties.monacoDisableRange ? decorations.concat([ decorationsToAdd.push({
{
range: this.guidedProperties.monacoDisableRange, range: this.guidedProperties.monacoDisableRange,
options: { options: {
isWholeLine: true, isWholeLine: true,
inlineClassName: "disable-text" inlineClassName: "disable-text"
}, },
className: "disable-text", className: "disable-text",
}, });
]) : decorations; }
this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, decorations)
this.decorations.set(decorationsToAdd);
} else { } else {
this.highlightPebble(); this.highlightPebble();
} }
@@ -363,14 +364,14 @@
highlightPebble() { highlightPebble() {
// Highlight code that match pebble content // Highlight code that match pebble content
let model = this.editor.getModel(); let model = this.editor.getModel();
let decorations = [];
let text = model.getValue(); let text = model.getValue();
let regex = new RegExp("\\{\\{(.+?)}}", "g"); let regex = new RegExp("\\{\\{(.+?)}}", "g");
let match; let match;
const decorationsToAdd = [];
while ((match = regex.exec(text)) !== null) { while ((match = regex.exec(text)) !== null) {
let startPos = model.getPositionAt(match.index); let startPos = model.getPositionAt(match.index);
let endPos = model.getPositionAt(match.index + match[0].length); let endPos = model.getPositionAt(match.index + match[0].length);
decorations.push({ decorationsToAdd.push({
range: { range: {
startLineNumber: startPos.lineNumber, startLineNumber: startPos.lineNumber,
startColumn: startPos.column, startColumn: startPos.column,
@@ -382,7 +383,7 @@
} }
}); });
} }
this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, decorations); this.decorations.set(decorationsToAdd);
} }
}, },
}; };

View File

@@ -809,7 +809,7 @@
ref="editorDomElement" ref="editorDomElement"
v-if="combinedEditor || viewType === editorViewTypes.SOURCE" v-if="combinedEditor || viewType === editorViewTypes.SOURCE"
:class="combinedEditor ? 'editor-combined' : ''" :class="combinedEditor ? 'editor-combined' : ''"
:style="combinedEditor ? {'flex-basis': leftEditorWidth, 'flex-grow': 0} : {}" :style="combinedEditor ? {'flex': '0 0 ' + leftEditorWidth} : {}"
@save="save" @save="save"
@execute="execute" @execute="execute"
v-model="flowYaml" v-model="flowYaml"

View File

@@ -45,6 +45,7 @@
import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue"; import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
import {mapState} from "vuex"; import {mapState} from "vuex";
import ChartBoxOutline from "vue-material-design-icons/ChartBoxOutline.vue"; import ChartBoxOutline from "vue-material-design-icons/ChartBoxOutline.vue";
import Connection from "vue-material-design-icons/Connection.vue";
import {shallowRef} from "vue"; import {shallowRef} from "vue";
export default { export default {
@@ -169,6 +170,15 @@
class: "menu-icon" class: "menu-icon"
}, },
}, },
{
href: {name: "plugins/list"},
routes: this.routeStartWith("plugins"),
title: this.$t("plugins.names"),
icon: {
element: shallowRef(Connection),
class: "menu-icon"
},
},
{ {
title: this.$t("administration"), title: this.$t("administration"),
routes: this.routeStartWith("admin"), routes: this.routeStartWith("admin"),

View File

@@ -4,7 +4,6 @@ import io.micronaut.http.HttpStatus;
import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.http.exceptions.HttpStatusException;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -15,7 +14,7 @@ public class RequestUtils {
.stream() .stream()
.map(s -> { .map(s -> {
String[] split = s.split("[: ]+"); String[] split = s.split("[: ]+");
if (split.length < 2) { if (split.length < 2 || split[0] == null || split[0].isEmpty()) {
throw new HttpStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Invalid queryString parameter"); throw new HttpStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Invalid queryString parameter");
} }

View File

@@ -24,10 +24,7 @@ import io.kestra.webserver.responses.BulkResponse;
import io.kestra.webserver.responses.PagedResults; import io.kestra.webserver.responses.PagedResults;
import io.micronaut.core.type.Argument; import io.micronaut.core.type.Argument;
import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable;
import io.micronaut.http.HttpRequest; import io.micronaut.http.*;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.client.multipart.MultipartBody; import io.micronaut.http.client.multipart.MultipartBody;
@@ -1086,4 +1083,21 @@ class ExecutionControllerTest extends JdbcH2ControllerTest {
assertThat(response.getCount(), is(3)); assertThat(response.getCount(), is(3));
} }
@Test
void nullLabels() {
MultipartBody requestBody = createInputsFlowBody();
// null keys are forbidden
MutableHttpRequest<MultipartBody> requestNullKey = HttpRequest
.POST("/api/v1/executions/" + TESTS_FLOW_NS + "/inputs?labels=:value", requestBody)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE);
assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(requestNullKey, Execution.class));
// null values are forbidden
MutableHttpRequest<MultipartBody> requestNullValue = HttpRequest
.POST("/api/v1/executions/" + TESTS_FLOW_NS + "/inputs?labels=key:", requestBody)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE);
assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(requestNullValue, Execution.class));
}
} }