mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
13 Commits
docs/purge
...
feat/entit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63e11c7d94 | ||
|
|
11e199da33 | ||
|
|
5d5165b7b9 | ||
|
|
44d0c10713 | ||
|
|
167734e32a | ||
|
|
24e61c81c0 | ||
|
|
379764a033 | ||
|
|
d55dd275c3 | ||
|
|
f409657e8a | ||
|
|
22f0b3ffdf | ||
|
|
0d99dc6862 | ||
|
|
fd3adc48b8 | ||
|
|
1a8a47c8cd |
2
.github/workflows/vulnerabilities-check.yml
vendored
2
.github/workflows/vulnerabilities-check.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
# Upload dependency check report
|
||||
- name: Upload dependency check report
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: dependency-check-report
|
||||
|
||||
@@ -137,6 +137,11 @@ flyway:
|
||||
# We must ignore missing migrations as we delete some wrong or not used anymore migrations
|
||||
ignore-migration-patterns: "*:missing,*:future"
|
||||
out-of-order: true
|
||||
properties:
|
||||
flyway:
|
||||
postgresql:
|
||||
transactional:
|
||||
lock: false
|
||||
mysql:
|
||||
enabled: true
|
||||
locations:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.kestra.core.exceptions;
|
||||
|
||||
import io.kestra.core.models.flows.Data;
|
||||
import io.kestra.core.models.flows.Input;
|
||||
import io.kestra.core.models.flows.Output;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Exception that can be thrown when Inputs/Outputs have validation problems.
|
||||
*/
|
||||
public class InputOutputValidationException extends KestraRuntimeException {
|
||||
public InputOutputValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public static InputOutputValidationException of( String message, Input<?> input){
|
||||
String inputMessage = "Invalid value for input" + " `" + input.getId() + "`. Cause: " + message;
|
||||
return new InputOutputValidationException(inputMessage);
|
||||
}
|
||||
public static InputOutputValidationException of( String message, Output output){
|
||||
String outputMessage = "Invalid value for output" + " `" + output.getId() + "`. Cause: " + message;
|
||||
return new InputOutputValidationException(outputMessage);
|
||||
}
|
||||
public static InputOutputValidationException of(String message){
|
||||
return new InputOutputValidationException(message);
|
||||
}
|
||||
|
||||
public static InputOutputValidationException merge(Set<InputOutputValidationException> exceptions){
|
||||
String combinedMessage = exceptions.stream()
|
||||
.map(InputOutputValidationException::getMessage)
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
throw new InputOutputValidationException(combinedMessage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.kestra.core.exceptions;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The top-level {@link KestraRuntimeException} for non-recoverable errors.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package io.kestra.core.models;
|
||||
|
||||
public interface DeletedInterface {
|
||||
boolean isDeleted();
|
||||
}
|
||||
18
core/src/main/java/io/kestra/core/models/SoftDeletable.java
Normal file
18
core/src/main/java/io/kestra/core/models/SoftDeletable.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package io.kestra.core.models;
|
||||
|
||||
/**
|
||||
* This interface marks entities that implement a soft deletion mechanism.
|
||||
* Soft deletion is based on a <code>deleted</code> field that is set to <code>true</code> when the entity is deleted.
|
||||
* Physical deletion either never occurs or occurs in a dedicated purge mechanism.
|
||||
*/
|
||||
public interface SoftDeletable<T> {
|
||||
/**
|
||||
* Whether en entity is deleted or not.
|
||||
*/
|
||||
boolean isDeleted();
|
||||
|
||||
/**
|
||||
* Delete the current entity: set its <code>deleted</code> field to <code>true</code>.
|
||||
*/
|
||||
T toDeleted();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.kestra.core.models.dashboards;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
@@ -26,7 +26,7 @@ import java.util.Objects;
|
||||
@NoArgsConstructor
|
||||
@Introspected
|
||||
@ToString
|
||||
public class Dashboard implements HasUID, DeletedInterface {
|
||||
public class Dashboard implements HasUID, SoftDeletable<Dashboard> {
|
||||
@Hidden
|
||||
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
|
||||
private String tenantId;
|
||||
@@ -71,6 +71,7 @@ public class Dashboard implements HasUID, DeletedInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dashboard toDeleted() {
|
||||
return this.toBuilder()
|
||||
.deleted(true)
|
||||
|
||||
@@ -11,7 +11,7 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Streams;
|
||||
import io.kestra.core.debug.Breakpoint;
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
@@ -53,7 +53,7 @@ import java.util.zip.CRC32;
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class Execution implements DeletedInterface, TenantInterface {
|
||||
public class Execution implements SoftDeletable<Execution>, TenantInterface {
|
||||
|
||||
@With
|
||||
@Hidden
|
||||
@@ -1111,7 +1111,7 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Execution toDeleted() {
|
||||
return this.toBuilder()
|
||||
.deleted(true)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.kestra.core.models.executions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
@@ -22,7 +21,7 @@ import java.util.stream.Stream;
|
||||
|
||||
@Value
|
||||
@Builder(toBuilder = true)
|
||||
public class LogEntry implements DeletedInterface, TenantInterface {
|
||||
public class LogEntry implements TenantInterface {
|
||||
@Hidden
|
||||
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
|
||||
String tenantId;
|
||||
@@ -57,10 +56,6 @@ public class LogEntry implements DeletedInterface, TenantInterface {
|
||||
|
||||
String message;
|
||||
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
boolean deleted = false;
|
||||
|
||||
@Nullable
|
||||
ExecutionKind executionKind;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.kestra.core.models.executions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.executions.metrics.Counter;
|
||||
import io.kestra.core.models.executions.metrics.Gauge;
|
||||
@@ -18,7 +17,7 @@ import jakarta.validation.constraints.Pattern;
|
||||
|
||||
@Value
|
||||
@Builder(toBuilder = true)
|
||||
public class MetricEntry implements DeletedInterface, TenantInterface {
|
||||
public class MetricEntry implements TenantInterface {
|
||||
@Hidden
|
||||
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
|
||||
String tenantId;
|
||||
@@ -54,10 +53,6 @@ public class MetricEntry implements DeletedInterface, TenantInterface {
|
||||
@Nullable
|
||||
Map<String, String> tags;
|
||||
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
boolean deleted = false;
|
||||
|
||||
@Nullable
|
||||
ExecutionKind executionKind;
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ package io.kestra.core.models.executions;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.tasks.FlowableTask;
|
||||
import io.kestra.core.models.tasks.ResolvedTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
@@ -95,8 +93,16 @@ public class TaskRun implements TenantInterface {
|
||||
this.forceExecution
|
||||
);
|
||||
}
|
||||
public TaskRun withStateAndAttempt(State.Type state) {
|
||||
List<TaskRunAttempt> newAttempts = new ArrayList<>(this.attempts != null ? this.attempts : List.of());
|
||||
|
||||
if (newAttempts.isEmpty()) {
|
||||
newAttempts.add(TaskRunAttempt.builder().state(new State(state)).build());
|
||||
} else {
|
||||
TaskRunAttempt updatedLast = newAttempts.getLast().withState(state);
|
||||
newAttempts.set(newAttempts.size() - 1, updatedLast);
|
||||
}
|
||||
|
||||
public TaskRun replaceState(State newState) {
|
||||
return new TaskRun(
|
||||
this.tenantId,
|
||||
this.id,
|
||||
@@ -106,9 +112,9 @@ public class TaskRun implements TenantInterface {
|
||||
this.taskId,
|
||||
this.parentTaskRunId,
|
||||
this.value,
|
||||
this.attempts,
|
||||
newAttempts,
|
||||
this.outputs,
|
||||
newState,
|
||||
this.state.withState(state),
|
||||
this.iteration,
|
||||
this.dynamic,
|
||||
this.forceExecution
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
/**
|
||||
* Interface for defining an identifiable and typed data.
|
||||
@@ -29,16 +27,4 @@ public interface Data {
|
||||
*/
|
||||
String getDisplayName();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default ConstraintViolationException toConstraintViolationException(String message, Object value) {
|
||||
Class<Data> cls = (Class<Data>) this.getClass();
|
||||
|
||||
return ManualConstraintViolation.toConstraintViolationException(
|
||||
"Invalid " + (this instanceof Output ? "output" : "input") + " for `" + getId() + "`, " + message + ", but received `" + value + "`",
|
||||
this,
|
||||
cls,
|
||||
this.getId(),
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +342,7 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flow toDeleted() {
|
||||
return this.toBuilder()
|
||||
.revision(this.revision + 1)
|
||||
|
||||
@@ -58,4 +58,9 @@ public class FlowForExecution extends AbstractFlow {
|
||||
public String getSource() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowForExecution toDeleted() {
|
||||
throw new UnsupportedOperationException("Can't delete a FlowForExecution");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.HasSource;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.Label;
|
||||
@@ -27,7 +27,7 @@ import java.util.stream.Collectors;
|
||||
* The base interface for FLow.
|
||||
*/
|
||||
@JsonDeserialize(as = GenericFlow.class)
|
||||
public interface FlowInterface extends FlowId, DeletedInterface, TenantInterface, HasUID, HasSource {
|
||||
public interface FlowInterface extends FlowId, SoftDeletable<FlowInterface>, TenantInterface, HasUID, HasSource {
|
||||
|
||||
Pattern YAML_REVISION_MATCHER = Pattern.compile("(?m)^revision: \\d+\n?");
|
||||
|
||||
|
||||
@@ -96,4 +96,9 @@ public class GenericFlow extends AbstractFlow implements HasUID {
|
||||
public List<GenericTrigger> getTriggers() {
|
||||
return Optional.ofNullable(triggers).orElse(List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowInterface toDeleted() {
|
||||
throw new UnsupportedOperationException("Can't delete a GenericFlow");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package io.kestra.core.models.flows.input;
|
||||
|
||||
import io.kestra.core.exceptions.InputOutputValidationException;
|
||||
import io.kestra.core.models.flows.Input;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents an input along with its associated value and validation state.
|
||||
*
|
||||
@@ -12,15 +14,15 @@ import jakarta.validation.constraints.NotNull;
|
||||
* @param value The provided value for the input.
|
||||
* @param enabled {@code true} if the input is enabled; {@code false} otherwise.
|
||||
* @param isDefault {@code true} if the provided value is the default; {@code false} otherwise.
|
||||
* @param exception The validation exception, if the input value is invalid; {@code null} otherwise.
|
||||
* @param exceptions The validation exceptions, if the input value is invalid; {@code null} otherwise.
|
||||
*/
|
||||
public record InputAndValue(
|
||||
Input<?> input,
|
||||
Object value,
|
||||
boolean enabled,
|
||||
boolean isDefault,
|
||||
ConstraintViolationException exception) {
|
||||
|
||||
Set<InputOutputValidationException> exceptions) {
|
||||
|
||||
/**
|
||||
* Creates a new {@link InputAndValue} instance.
|
||||
*
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import io.kestra.core.validations.Regex;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
@@ -14,10 +15,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
@SuperBuilder
|
||||
@@ -77,30 +75,35 @@ public class MultiselectInput extends Input<List<String>> implements ItemTypeInt
|
||||
|
||||
@Override
|
||||
public void validate(List<String> inputs) throws ConstraintViolationException {
|
||||
Set<ConstraintViolation<?>> violations = new HashSet<>();
|
||||
|
||||
if (values != null && options != null) {
|
||||
throw ManualConstraintViolation.toConstraintViolationException(
|
||||
violations.add( ManualConstraintViolation.of(
|
||||
"you can't define both `values` and `options`",
|
||||
this,
|
||||
MultiselectInput.class,
|
||||
getId(),
|
||||
""
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
if (!this.getAllowCustomValue()) {
|
||||
for (String input : inputs) {
|
||||
List<@Regex String> finalValues = this.values != null ? this.values : this.options;
|
||||
if (!finalValues.contains(input)) {
|
||||
throw ManualConstraintViolation.toConstraintViolationException(
|
||||
"it must match the values `" + finalValues + "`",
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"value `" + input + "` doesn't match the values `" + finalValues + "`",
|
||||
this,
|
||||
MultiselectInput.class,
|
||||
getId(),
|
||||
input
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!violations.isEmpty()) {
|
||||
throw ManualConstraintViolation.toConstraintViolationException(violations);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@@ -145,7 +148,7 @@ public class MultiselectInput extends Input<List<String>> implements ItemTypeInt
|
||||
|
||||
String type = Optional.ofNullable(result).map(Object::getClass).map(Class::getSimpleName).orElse("<null>");
|
||||
throw ManualConstraintViolation.toConstraintViolationException(
|
||||
"Invalid expression result. Expected a list of strings, but received " + type,
|
||||
"Invalid expression result. Expected a list of strings",
|
||||
this,
|
||||
MultiselectInput.class,
|
||||
getId(),
|
||||
|
||||
@@ -125,7 +125,7 @@ public class SelectInput extends Input<String> implements RenderableInput {
|
||||
|
||||
String type = Optional.ofNullable(result).map(Object::getClass).map(Class::getSimpleName).orElse("<null>");
|
||||
throw ManualConstraintViolation.toConstraintViolationException(
|
||||
"Invalid expression result. Expected a list of strings, but received " + type,
|
||||
"Invalid expression result. Expected a list of strings",
|
||||
this,
|
||||
SelectInput.class,
|
||||
getId(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.kestra.core.models.kv;
|
||||
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.storages.kv.KVEntry;
|
||||
@@ -22,7 +22,7 @@ import java.util.Optional;
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class PersistedKvMetadata implements DeletedInterface, TenantInterface, HasUID {
|
||||
public class PersistedKvMetadata implements SoftDeletable<PersistedKvMetadata>, TenantInterface, HasUID {
|
||||
@With
|
||||
@Hidden
|
||||
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
|
||||
@@ -83,6 +83,7 @@ public class PersistedKvMetadata implements DeletedInterface, TenantInterface, H
|
||||
return this.toBuilder().updated(Instant.now()).last(true).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistedKvMetadata toDeleted() {
|
||||
return this.toBuilder().updated(Instant.now()).deleted(true).build();
|
||||
}
|
||||
|
||||
@@ -17,8 +17,4 @@ public class Namespace implements NamespaceInterface {
|
||||
@NotNull
|
||||
@Pattern(regexp="^[a-z0-9][a-z0-9._-]*")
|
||||
protected String id;
|
||||
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
boolean deleted = false;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package io.kestra.core.models.namespaces;
|
||||
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.HasUID;
|
||||
|
||||
public interface NamespaceInterface extends DeletedInterface, HasUID {
|
||||
public interface NamespaceInterface extends HasUID {
|
||||
String getId();
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package io.kestra.core.models.namespaces.files;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.storages.FileAttributes;
|
||||
import io.kestra.core.storages.NamespaceFile;
|
||||
@@ -24,7 +24,7 @@ import java.time.Instant;
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class NamespaceFileMetadata implements DeletedInterface, TenantInterface, HasUID {
|
||||
public class NamespaceFileMetadata implements SoftDeletable<NamespaceFileMetadata>, TenantInterface, HasUID {
|
||||
@With
|
||||
@Hidden
|
||||
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
|
||||
@@ -116,6 +116,7 @@ public class NamespaceFileMetadata implements DeletedInterface, TenantInterface,
|
||||
return this.toBuilder().updated(saveDate).last(true).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NamespaceFileMetadata toDeleted() {
|
||||
return this.toBuilder().deleted(true).updated(Instant.now()).build();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.runners.RunContextProperty;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@@ -156,9 +157,9 @@ public class Property<T> {
|
||||
/**
|
||||
* Render a property, then convert it to its target type.<br>
|
||||
* <p>
|
||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||
* This method is designed to be used only by the {@link RunContextProperty}.
|
||||
*
|
||||
* @see io.kestra.core.runners.RunContextProperty#as(Class)
|
||||
* @see RunContextProperty#as(Class)
|
||||
*/
|
||||
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz) throws IllegalVariableEvaluationException {
|
||||
return as(property, context, clazz, Map.of());
|
||||
@@ -167,25 +168,57 @@ public class Property<T> {
|
||||
/**
|
||||
* Render a property with additional variables, then convert it to its target type.<br>
|
||||
* <p>
|
||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||
* This method is designed to be used only by the {@link RunContextProperty}.
|
||||
*
|
||||
* @see io.kestra.core.runners.RunContextProperty#as(Class, Map)
|
||||
* @see RunContextProperty#as(Class, Map)
|
||||
*/
|
||||
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
if (property.skipCache || property.value == null) {
|
||||
String rendered = context.render(property.expression, variables);
|
||||
property.value = MAPPER.convertValue(rendered, clazz);
|
||||
property.value = deserialize(rendered, clazz);
|
||||
}
|
||||
|
||||
return property.value;
|
||||
}
|
||||
|
||||
private static <T> T deserialize(Object rendered, Class<T> clazz) throws IllegalVariableEvaluationException {
|
||||
try {
|
||||
return MAPPER.convertValue(rendered, clazz);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (rendered instanceof String str) {
|
||||
try {
|
||||
return MAPPER.readValue(str, clazz);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new IllegalVariableEvaluationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T deserialize(Object rendered, JavaType type) throws IllegalVariableEvaluationException {
|
||||
try {
|
||||
return MAPPER.convertValue(rendered, type);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (rendered instanceof String str) {
|
||||
try {
|
||||
return MAPPER.readValue(str, type);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new IllegalVariableEvaluationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a property then convert it as a list of target type.<br>
|
||||
* <p>
|
||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||
* This method is designed to be used only by the {@link RunContextProperty}.
|
||||
*
|
||||
* @see io.kestra.core.runners.RunContextProperty#asList(Class)
|
||||
* @see RunContextProperty#asList(Class)
|
||||
*/
|
||||
public static <T, I> T asList(Property<T> property, PropertyContext context, Class<I> itemClazz) throws IllegalVariableEvaluationException {
|
||||
return asList(property, context, itemClazz, Map.of());
|
||||
@@ -194,37 +227,39 @@ public class Property<T> {
|
||||
/**
|
||||
* Render a property with additional variables, then convert it as a list of target type.<br>
|
||||
* <p>
|
||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||
* This method is designed to be used only by the {@link RunContextProperty}.
|
||||
*
|
||||
* @see io.kestra.core.runners.RunContextProperty#asList(Class, Map)
|
||||
* @see RunContextProperty#asList(Class, Map)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T, I> T asList(Property<T> property, PropertyContext context, Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
if (property.skipCache || property.value == null) {
|
||||
JavaType type = MAPPER.getTypeFactory().constructCollectionLikeType(List.class, itemClazz);
|
||||
try {
|
||||
String trimmedExpression = property.expression.trim();
|
||||
// We need to detect if the expression is already a list or if it's a pebble expression (for eg. referencing a variable containing a list).
|
||||
// Doing that allows us to, if it's an expression, first render then read it as a list.
|
||||
if (trimmedExpression.startsWith("{{") && trimmedExpression.endsWith("}}")) {
|
||||
property.value = MAPPER.readValue(context.render(property.expression, variables), type);
|
||||
}
|
||||
// Otherwise, if it's already a list, we read it as a list first then render it from run context which handle list rendering by rendering each item of the list
|
||||
else {
|
||||
List<?> asRawList = MAPPER.readValue(property.expression, List.class);
|
||||
property.value = (T) asRawList.stream()
|
||||
.map(throwFunction(item -> {
|
||||
if (item instanceof String str) {
|
||||
return MAPPER.convertValue(context.render(str, variables), itemClazz);
|
||||
} else if (item instanceof Map map) {
|
||||
return MAPPER.convertValue(context.render(map, variables), itemClazz);
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
.toList();
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
String trimmedExpression = property.expression.trim();
|
||||
// We need to detect if the expression is already a list or if it's a pebble expression (for eg. referencing a variable containing a list).
|
||||
// Doing that allows us to, if it's an expression, first render then read it as a list.
|
||||
if (trimmedExpression.startsWith("{{") && trimmedExpression.endsWith("}}")) {
|
||||
property.value = deserialize(context.render(property.expression, variables), type);
|
||||
}
|
||||
// Otherwise, if it's already a list, we read it as a list first then render it from run context which handle list rendering by rendering each item of the list
|
||||
else {
|
||||
List<?> asRawList = deserialize(property.expression, List.class);
|
||||
property.value = (T) asRawList.stream()
|
||||
.map(throwFunction(item -> {
|
||||
Object rendered = null;
|
||||
if (item instanceof String str) {
|
||||
rendered = context.render(str, variables);
|
||||
} else if (item instanceof Map map) {
|
||||
rendered = context.render(map, variables);
|
||||
}
|
||||
|
||||
if (rendered != null) {
|
||||
return deserialize(rendered, itemClazz);
|
||||
}
|
||||
|
||||
return item;
|
||||
}))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,9 +269,9 @@ public class Property<T> {
|
||||
/**
|
||||
* Render a property then convert it as a map of target types.<br>
|
||||
* <p>
|
||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||
* This method is designed to be used only by the {@link RunContextProperty}.
|
||||
*
|
||||
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class)
|
||||
* @see RunContextProperty#asMap(Class, Class)
|
||||
*/
|
||||
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
|
||||
return asMap(property, runContext, keyClass, valueClass, Map.of());
|
||||
@@ -248,7 +283,7 @@ public class Property<T> {
|
||||
* This method is safe to be used as many times as you want as the rendering and conversion will be cached.
|
||||
* Warning, due to the caching mechanism, this method is not thread-safe.
|
||||
*
|
||||
* @see io.kestra.core.runners.RunContextProperty#asMap(Class, Class, Map)
|
||||
* @see RunContextProperty#asMap(Class, Class, Map)
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
@@ -260,12 +295,12 @@ public class Property<T> {
|
||||
// We need to detect if the expression is already a map or if it's a pebble expression (for eg. referencing a variable containing a map).
|
||||
// Doing that allows us to, if it's an expression, first render then read it as a map.
|
||||
if (trimmedExpression.startsWith("{{") && trimmedExpression.endsWith("}}")) {
|
||||
property.value = MAPPER.readValue(runContext.render(property.expression, variables), targetMapType);
|
||||
property.value = deserialize(runContext.render(property.expression, variables), targetMapType);
|
||||
}
|
||||
// Otherwise if it's already a map we read it as a map first then render it from run context which handle map rendering by rendering each entry of the map (otherwise it will fail with nested expressions in values for eg.)
|
||||
else {
|
||||
Map asRawMap = MAPPER.readValue(property.expression, Map.class);
|
||||
property.value = MAPPER.convertValue(runContext.render(asRawMap, variables), targetMapType);
|
||||
property.value = deserialize(runContext.render(asRawMap, variables), targetMapType);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
|
||||
@@ -7,8 +7,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
|
||||
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
@@ -35,7 +35,7 @@ import jakarta.validation.constraints.Pattern;
|
||||
@Introspected
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class Template implements DeletedInterface, TenantInterface, HasUID {
|
||||
public class Template implements SoftDeletable<Template>, TenantInterface, HasUID {
|
||||
private static final ObjectMapper YAML_MAPPER = JacksonMapper.ofYaml().copy()
|
||||
.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
|
||||
@Override
|
||||
@@ -141,6 +141,7 @@ public class Template implements DeletedInterface, TenantInterface, HasUID {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Template toDeleted() {
|
||||
return new Template(
|
||||
this.tenantId,
|
||||
|
||||
@@ -67,6 +67,11 @@ public class ManualConstraintViolation<T> implements ConstraintViolation<T> {
|
||||
invalidValue
|
||||
)));
|
||||
}
|
||||
public static <T> ConstraintViolationException toConstraintViolationException(
|
||||
Set<? extends ConstraintViolation<?>> constraintViolations
|
||||
) {
|
||||
return new ConstraintViolationException(constraintViolations);
|
||||
}
|
||||
|
||||
public String getMessageTemplate() {
|
||||
return "{messageTemplate}";
|
||||
|
||||
@@ -36,7 +36,7 @@ public interface KvMetadataRepositoryInterface extends SaveRepositoryInterface<P
|
||||
);
|
||||
|
||||
default PersistedKvMetadata delete(PersistedKvMetadata persistedKvMetadata) throws IOException {
|
||||
return this.save(persistedKvMetadata.toBuilder().deleted(true).build());
|
||||
return this.save(persistedKvMetadata.toDeleted());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ package io.kestra.core.runners;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.kestra.core.encryption.EncryptionService;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
|
||||
import io.kestra.core.exceptions.InputOutputValidationException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Data;
|
||||
import io.kestra.core.models.flows.DependsOn;
|
||||
@@ -17,7 +19,6 @@ import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.property.PropertyContext;
|
||||
import io.kestra.core.models.property.URIFetcher;
|
||||
import io.kestra.core.models.tasks.common.EncryptedString;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.storages.StorageContext;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
@@ -207,8 +208,8 @@ public class FlowInputOutput {
|
||||
.filter(InputAndValue::enabled)
|
||||
.map(it -> {
|
||||
//TODO check to return all exception at-once.
|
||||
if (it.exception() != null) {
|
||||
throw it.exception();
|
||||
if (it.exceptions() != null && !it.exceptions().isEmpty()) {
|
||||
throw InputOutputValidationException.merge(it.exceptions());
|
||||
}
|
||||
return new AbstractMap.SimpleEntry<>(it.input().getId(), it.value());
|
||||
})
|
||||
@@ -292,13 +293,9 @@ public class FlowInputOutput {
|
||||
try {
|
||||
isInputEnabled = Boolean.TRUE.equals(runContext.renderTyped(dependsOnCondition.get()));
|
||||
} catch (IllegalVariableEvaluationException e) {
|
||||
resolvable.resolveWithError(ManualConstraintViolation.toConstraintViolationException(
|
||||
"Invalid condition: " + e.getMessage(),
|
||||
input,
|
||||
(Class<Input>)input.getClass(),
|
||||
input.getId(),
|
||||
this
|
||||
));
|
||||
resolvable.resolveWithError(
|
||||
InputOutputValidationException.of("Invalid condition: " + e.getMessage())
|
||||
);
|
||||
isInputEnabled = false;
|
||||
}
|
||||
}
|
||||
@@ -331,7 +328,7 @@ public class FlowInputOutput {
|
||||
// validate and parse input value
|
||||
if (value == null) {
|
||||
if (input.getRequired()) {
|
||||
resolvable.resolveWithError(input.toConstraintViolationException("missing required input", null));
|
||||
resolvable.resolveWithError(InputOutputValidationException.of("Missing required input:" + input.getId()));
|
||||
} else {
|
||||
resolvable.resolveWithValue(null);
|
||||
}
|
||||
@@ -341,17 +338,18 @@ public class FlowInputOutput {
|
||||
parsedInput.ifPresent(parsed -> ((Input) resolvable.get().input()).validate(parsed.getValue()));
|
||||
parsedInput.ifPresent(typed -> resolvable.resolveWithValue(typed.getValue()));
|
||||
} catch (ConstraintViolationException e) {
|
||||
ConstraintViolationException exception = e.getConstraintViolations().size() == 1 ?
|
||||
input.toConstraintViolationException(List.copyOf(e.getConstraintViolations()).getFirst().getMessage(), value) :
|
||||
input.toConstraintViolationException(e.getMessage(), value);
|
||||
resolvable.resolveWithError(exception);
|
||||
Input<?> finalInput = input;
|
||||
Set<InputOutputValidationException> exceptions = e.getConstraintViolations().stream()
|
||||
.map(c-> InputOutputValidationException.of(c.getMessage(), finalInput))
|
||||
.collect(Collectors.toSet());
|
||||
resolvable.resolveWithError(exceptions);
|
||||
}
|
||||
}
|
||||
} catch (ConstraintViolationException e) {
|
||||
resolvable.resolveWithError(e);
|
||||
} catch (Exception e) {
|
||||
ConstraintViolationException exception = input.toConstraintViolationException(e instanceof IllegalArgumentException ? e.getMessage() : e.toString(), resolvable.get().value());
|
||||
resolvable.resolveWithError(exception);
|
||||
} catch (IllegalArgumentException e){
|
||||
resolvable.resolveWithError(InputOutputValidationException.of(e.getMessage(), input));
|
||||
}
|
||||
catch (Exception e) {
|
||||
resolvable.resolveWithError(InputOutputValidationException.of(e.getMessage()));
|
||||
}
|
||||
|
||||
return resolvable.get();
|
||||
@@ -439,8 +437,12 @@ public class FlowInputOutput {
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw output.toConstraintViolationException(e.getMessage(), current);
|
||||
}
|
||||
catch (IllegalArgumentException e){
|
||||
throw InputOutputValidationException.of(e.getMessage(), output);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw InputOutputValidationException.of(e.getMessage());
|
||||
}
|
||||
})
|
||||
.filter(Optional::isPresent)
|
||||
@@ -503,7 +505,7 @@ public class FlowInputOutput {
|
||||
if (matcher.matches()) {
|
||||
yield current.toString();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Expected `URI` but received `" + current + "`");
|
||||
throw new IllegalArgumentException("Invalid URI format.");
|
||||
}
|
||||
}
|
||||
case ARRAY, MULTISELECT -> {
|
||||
@@ -533,7 +535,7 @@ public class FlowInputOutput {
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new Exception("Expected `" + type + "` but received `" + current + "` with errors:\n```\n" + e.getMessage() + "\n```");
|
||||
throw new Exception(" errors:\n```\n" + e.getMessage() + "\n```");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,27 +567,30 @@ public class FlowInputOutput {
|
||||
}
|
||||
|
||||
public void isDefault(boolean isDefault) {
|
||||
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), isDefault, this.input.exception());
|
||||
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), isDefault, this.input.exceptions());
|
||||
}
|
||||
|
||||
public void setInput(final Input<?> input) {
|
||||
this.input = new InputAndValue(input, this.input.value(), this.input.enabled(), this.input.isDefault(), this.input.exception());
|
||||
this.input = new InputAndValue(input, this.input.value(), this.input.enabled(), this.input.isDefault(), this.input.exceptions());
|
||||
}
|
||||
|
||||
public void resolveWithEnabled(boolean enabled) {
|
||||
this.input = new InputAndValue(this.input.input(), input.value(), enabled, this.input.isDefault(), this.input.exception());
|
||||
this.input = new InputAndValue(this.input.input(), input.value(), enabled, this.input.isDefault(), this.input.exceptions());
|
||||
markAsResolved();
|
||||
}
|
||||
|
||||
public void resolveWithValue(@Nullable Object value) {
|
||||
this.input = new InputAndValue(this.input.input(), value, this.input.enabled(), this.input.isDefault(), this.input.exception());
|
||||
this.input = new InputAndValue(this.input.input(), value, this.input.enabled(), this.input.isDefault(), this.input.exceptions());
|
||||
markAsResolved();
|
||||
}
|
||||
|
||||
public void resolveWithError(@Nullable ConstraintViolationException exception) {
|
||||
public void resolveWithError(@Nullable Set<InputOutputValidationException> exception) {
|
||||
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), this.input.isDefault(), exception);
|
||||
markAsResolved();
|
||||
}
|
||||
private void resolveWithError(@Nullable InputOutputValidationException exception){
|
||||
resolveWithError(Collections.singleton(exception));
|
||||
}
|
||||
|
||||
private void markAsResolved() {
|
||||
this.isResolved = true;
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.micronaut.core.annotation.Nullable;
|
||||
import io.pebbletemplates.pebble.PebbleEngine;
|
||||
import io.pebbletemplates.pebble.extension.Extension;
|
||||
import io.pebbletemplates.pebble.extension.Function;
|
||||
import io.pebbletemplates.pebble.lexer.Syntax;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -37,6 +38,13 @@ public class PebbleEngineFactory {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public PebbleEngine createWithCustomSyntax(Syntax syntax, Class<? extends Extension> extension) {
|
||||
PebbleEngine.Builder builder = newPebbleEngineBuilder()
|
||||
.syntax(syntax);
|
||||
this.applicationContext.getBeansOfType(extension).forEach(builder::extension);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public PebbleEngine createWithMaskedFunctions(VariableRenderer renderer, final List<String> functionsToMask) {
|
||||
|
||||
PebbleEngine.Builder builder = newPebbleEngineBuilder();
|
||||
|
||||
@@ -35,6 +35,10 @@ public final class YamlParser {
|
||||
return read(input, cls, type(cls));
|
||||
}
|
||||
|
||||
public static <T> T parse(String input, Class<T> cls, Boolean strict) {
|
||||
return strict ? read(input, cls, type(cls)) : readNonStrict(input, cls, type(cls));
|
||||
}
|
||||
|
||||
public static <T> T parse(Map<String, Object> input, Class<T> cls, Boolean strict) {
|
||||
ObjectMapper currentMapper = strict ? STRICT_MAPPER : NON_STRICT_MAPPER;
|
||||
|
||||
@@ -81,6 +85,13 @@ public final class YamlParser {
|
||||
throw toConstraintViolationException(input, resource, e);
|
||||
}
|
||||
}
|
||||
private static <T> T readNonStrict(String input, Class<T> objectClass, String resource) {
|
||||
try {
|
||||
return NON_STRICT_MAPPER.readValue(input, objectClass);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw toConstraintViolationException(input, resource, e);
|
||||
}
|
||||
}
|
||||
private static String formatYamlErrorMessage(String originalMessage, JsonProcessingException e) {
|
||||
StringBuilder friendlyMessage = new StringBuilder();
|
||||
if (originalMessage.contains("Expected a field name")) {
|
||||
|
||||
@@ -754,7 +754,7 @@ public class ExecutionService {
|
||||
var parentTaskRun = execution.findTaskRunByTaskRunId(taskRun.getParentTaskRunId());
|
||||
Execution newExecution = execution;
|
||||
if (parentTaskRun.getState().getCurrent() != State.Type.KILLED) {
|
||||
newExecution = newExecution.withTaskRun(parentTaskRun.withState(State.Type.KILLED));
|
||||
newExecution = newExecution.withTaskRun(parentTaskRun.withStateAndAttempt(State.Type.KILLED));
|
||||
}
|
||||
if (parentTaskRun.getParentTaskRunId() != null) {
|
||||
return killParentTaskruns(parentTaskRun, newExecution);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.kestra.core.test;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.HasSource;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
@@ -25,7 +25,7 @@ import java.util.List;
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@TestSuiteValidation
|
||||
public class TestSuite implements HasUID, TenantInterface, DeletedInterface, HasSource {
|
||||
public class TestSuite implements HasUID, TenantInterface, SoftDeletable<TestSuite>, HasSource {
|
||||
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@@ -85,10 +85,6 @@ public class TestSuite implements HasUID, TenantInterface, DeletedInterface, Has
|
||||
);
|
||||
}
|
||||
|
||||
public TestSuite delete() {
|
||||
return this.toBuilder().deleted(true).build();
|
||||
}
|
||||
|
||||
public TestSuite disable() {
|
||||
var disabled = true;
|
||||
return this.toBuilder()
|
||||
@@ -120,4 +116,9 @@ public class TestSuite implements HasUID, TenantInterface, DeletedInterface, Has
|
||||
|
||||
return yamlSource + String.format("\ndisabled: %s", disabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestSuite toDeleted() {
|
||||
return toBuilder().deleted(true).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.kestra.core.test;
|
||||
|
||||
import io.kestra.core.models.DeletedInterface;
|
||||
import io.kestra.core.models.SoftDeletable;
|
||||
import io.kestra.core.models.HasUID;
|
||||
import io.kestra.core.models.TenantInterface;
|
||||
import io.kestra.core.test.flow.UnitTestResult;
|
||||
@@ -24,7 +24,7 @@ public record TestSuiteRunEntity(
|
||||
String flowId,
|
||||
TestState state,
|
||||
List<UnitTestResult> results
|
||||
) implements DeletedInterface, TenantInterface, HasUID {
|
||||
) implements SoftDeletable<TestSuiteRunEntity>, TenantInterface, HasUID {
|
||||
|
||||
public static TestSuiteRunEntity create(String tenantId, TestSuiteUid testSuiteUid, TestSuiteRunResult testSuiteRunResult) {
|
||||
return new TestSuiteRunEntity(
|
||||
@@ -43,23 +43,6 @@ public record TestSuiteRunEntity(
|
||||
);
|
||||
}
|
||||
|
||||
public TestSuiteRunEntity delete() {
|
||||
return new TestSuiteRunEntity(
|
||||
this.uid,
|
||||
this.id,
|
||||
this.tenantId,
|
||||
true,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.testSuiteId,
|
||||
this.testSuiteUid,
|
||||
this.namespace,
|
||||
this.flowId,
|
||||
this.state,
|
||||
this.results
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* only used for backup
|
||||
* @param newTenantId the tenant to migrate to
|
||||
@@ -86,6 +69,24 @@ public record TestSuiteRunEntity(
|
||||
return this.deleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestSuiteRunEntity toDeleted() {
|
||||
return new TestSuiteRunEntity(
|
||||
this.uid,
|
||||
this.id,
|
||||
this.tenantId,
|
||||
true,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.testSuiteId,
|
||||
this.testSuiteUid,
|
||||
this.namespace,
|
||||
this.flowId,
|
||||
this.state,
|
||||
this.results
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTenantId() {
|
||||
return this.tenantId;
|
||||
|
||||
@@ -2,10 +2,8 @@ package io.kestra.plugin.core.namespace;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.kestra.core.repositories.NamespaceFileMetadataRepositoryInterface;
|
||||
import io.kestra.core.storages.Namespace;
|
||||
import io.kestra.core.storages.NamespaceFile;
|
||||
import io.kestra.plugin.core.kv.Version;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@@ -26,28 +26,25 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@Schema(
|
||||
title = "Purge namespace files for one or multiple namespaces.",
|
||||
description = "This task purges namespace files (and their versions) stored in Kestra. You can restrict the purge to specific namespaces (or a namespace glob pattern), optionally include child namespaces, and filter files by a glob pattern. The purge strategy is controlled via `behavior` (e.g. keep the last N versions and/or delete versions older than a given date)."
|
||||
title = "Delete expired keys globally for a specific namespace.",
|
||||
description = "This task will delete expired keys from the Kestra KV store. By default, it will only delete expired keys, but you can choose to delete all keys by setting `expiredOnly` to false. You can also filter keys by a specific pattern and choose to include child namespaces."
|
||||
)
|
||||
@Plugin(
|
||||
examples = {
|
||||
@Example(
|
||||
title = "Purge old versions of namespace files for a namespace tree.",
|
||||
title = "Delete expired keys globally for a specific namespace, with or without including child namespaces.",
|
||||
full = true,
|
||||
code = """
|
||||
id: purge_namespace_files
|
||||
id: purge_kv_store
|
||||
namespace: system
|
||||
|
||||
|
||||
tasks:
|
||||
- id: purge_files
|
||||
type: io.kestra.plugin.core.namespace.PurgeFiles
|
||||
- id: purge_kv
|
||||
type: io.kestra.plugin.core.kv.PurgeKV
|
||||
expiredOnly: true
|
||||
namespaces:
|
||||
- company
|
||||
includeChildNamespaces: true
|
||||
filePattern: "**/*.sql"
|
||||
behavior:
|
||||
type: version
|
||||
before: "2025-01-01T00:00:00Z"
|
||||
"""
|
||||
)
|
||||
}
|
||||
@@ -119,7 +116,7 @@ public class PurgeFiles extends Task implements PurgeTask<NamespaceFile>, Runnab
|
||||
@Getter
|
||||
public static class Output implements io.kestra.core.models.tasks.Output {
|
||||
@Schema(
|
||||
title = "The number of purged namespace file versions"
|
||||
title = "The number of purged KV pairs"
|
||||
)
|
||||
private Long size;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
package io.kestra.core.models.property;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.kestra.core.context.TestRunContextFactory;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.serializers.FileSerde;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.storages.Namespace;
|
||||
import io.kestra.core.storages.NamespaceFile;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.plugin.core.namespace.Version;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.event.Level;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||
import static java.util.Map.entry;
|
||||
@@ -362,10 +374,43 @@ class PropertyTest {
|
||||
assertThat(output.getMessages().getFirst().getValue()).isEqualTo("value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonSubtype() throws JsonProcessingException, IllegalVariableEvaluationException {
|
||||
Optional<WithSubtype> rendered = runContextFactory.of().render(
|
||||
Property.<WithSubtype>ofExpression(JacksonMapper.ofJson().writeValueAsString(new MySubtype()))
|
||||
).as(WithSubtype.class);
|
||||
|
||||
assertThat(rendered).isPresent();
|
||||
assertThat(rendered.get()).isInstanceOf(MySubtype.class);
|
||||
|
||||
List<WithSubtype> renderedList = runContextFactory.of().render(
|
||||
Property.<List<WithSubtype>>ofExpression(JacksonMapper.ofJson().writeValueAsString(List.of(new MySubtype())))
|
||||
).asList(WithSubtype.class);
|
||||
assertThat(renderedList).hasSize(1);
|
||||
assertThat(renderedList.get(0)).isInstanceOf(MySubtype.class);
|
||||
}
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = MySubtype.class, name = "mySubtype")
|
||||
})
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@Introspected
|
||||
public abstract static class WithSubtype {
|
||||
abstract public String getType();
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class MySubtype extends WithSubtype {
|
||||
private final String type = "mySubtype";
|
||||
}
|
||||
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
private static class TestObj {
|
||||
private String key;
|
||||
private String value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ public abstract class AbstractRunnerConcurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@FlakyTest(description = "Only flaky in CI")
|
||||
@LoadFlows(value = {"flows/valids/flow-concurrency-queue-killed.yml"}, tenantId = "flow-concurrency-killed")
|
||||
void flowConcurrencyKilled() throws Exception {
|
||||
flowConcurrencyCaseTest.flowConcurrencyKilled("flow-concurrency-killed");
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
@@ -466,4 +467,20 @@ class ExecutionServiceTest {
|
||||
assertThat(restart.getTaskRunList()).hasSize(2);
|
||||
assertThat(restart.findTaskRunsByTaskId("make_error").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/each-pause.yaml"})
|
||||
void killExecutionWithFlowableTask() throws Exception {
|
||||
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "each-pause");
|
||||
|
||||
TaskRun childTaskRun = execution.getTaskRunList().stream().filter(tr -> tr.getTaskId().equals("pause")).toList().getFirst();
|
||||
|
||||
Execution killed = executionService.killParentTaskruns(childTaskRun,execution);
|
||||
|
||||
TaskRun parentTaskRun = killed.getTaskRunList().stream().filter(tr -> tr.getTaskId().equals("each_task")).toList().getFirst();
|
||||
|
||||
assertThat(parentTaskRun.getState().getCurrent()).isEqualTo(State.Type.KILLED);
|
||||
assertThat(parentTaskRun.getAttempts().getLast().getState().getCurrent()).isEqualTo(State.Type.KILLED);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -239,7 +239,7 @@ class FlowInputOutputTest {
|
||||
// Then
|
||||
Assertions.assertEquals(2, values.size());
|
||||
Assertions.assertFalse(values.get(1).enabled());
|
||||
Assertions.assertNotNull(values.get(1).exception());
|
||||
Assertions.assertNotNull(values.get(1).exceptions());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -257,7 +257,7 @@ class FlowInputOutputTest {
|
||||
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, data).block();
|
||||
|
||||
// Then
|
||||
Assertions.assertNull(values.getFirst().exception());
|
||||
Assertions.assertNull(values.getFirst().exceptions());
|
||||
Assertions.assertFalse(storageInterface.exists(MAIN_TENANT, null, URI.create(values.getFirst().value().toString())));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.core.runners;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.CharStreams;
|
||||
import io.kestra.core.exceptions.InputOutputValidationException;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
@@ -137,8 +138,8 @@ public class InputsTest {
|
||||
void missingRequired() {
|
||||
HashMap<String, Object> inputs = new HashMap<>(InputsTest.inputs);
|
||||
inputs.put("string", null);
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(inputs, MAIN_TENANT));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `string`, missing required input, but received `null`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(inputs, MAIN_TENANT));
|
||||
assertThat(e.getMessage()).contains("Missing required input:string");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -232,9 +233,9 @@ public class InputsTest {
|
||||
HashMap<String, Object> map = new HashMap<>(inputs);
|
||||
map.put("validatedString", "foo");
|
||||
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(map, "tenant4"));
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(map, "tenant4"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedString`, it must match the pattern");
|
||||
assertThat(e.getMessage()).contains( "Invalid value for input `validatedString`. Cause: it must match the pattern");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -242,15 +243,15 @@ public class InputsTest {
|
||||
void inputValidatedIntegerBadValue() {
|
||||
HashMap<String, Object> mapMin = new HashMap<>(inputs);
|
||||
mapMin.put("validatedInt", "9");
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMin, "tenant5"));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedInt`, it must be more than `10`, but received `9`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMin, "tenant5"));
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedInt`. Cause: it must be more than `10`");
|
||||
|
||||
HashMap<String, Object> mapMax = new HashMap<>(inputs);
|
||||
mapMax.put("validatedInt", "21");
|
||||
|
||||
e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMax, "tenant5"));
|
||||
e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMax, "tenant5"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedInt`, it must be less than `20`, but received `21`");
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedInt`. Cause: it must be less than `20`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -258,15 +259,15 @@ public class InputsTest {
|
||||
void inputValidatedDateBadValue() {
|
||||
HashMap<String, Object> mapMin = new HashMap<>(inputs);
|
||||
mapMin.put("validatedDate", "2022-01-01");
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMin, "tenant6"));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedDate`, it must be after `2023-01-01`, but received `2022-01-01`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMin, "tenant6"));
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedDate`. Cause: it must be after `2023-01-01`");
|
||||
|
||||
HashMap<String, Object> mapMax = new HashMap<>(inputs);
|
||||
mapMax.put("validatedDate", "2024-01-01");
|
||||
|
||||
e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMax, "tenant6"));
|
||||
e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMax, "tenant6"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedDate`, it must be before `2023-12-31`, but received `2024-01-01`");
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedDate`. Cause: it must be before `2023-12-31`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -274,15 +275,15 @@ public class InputsTest {
|
||||
void inputValidatedDateTimeBadValue() {
|
||||
HashMap<String, Object> mapMin = new HashMap<>(inputs);
|
||||
mapMin.put("validatedDateTime", "2022-01-01T00:00:00Z");
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMin, "tenant7"));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedDateTime`, it must be after `2023-01-01T00:00:00Z`, but received `2022-01-01T00:00:00Z`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMin, "tenant7"));
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedDateTime`. Cause: it must be after `2023-01-01T00:00:00Z`");
|
||||
|
||||
HashMap<String, Object> mapMax = new HashMap<>(inputs);
|
||||
mapMax.put("validatedDateTime", "2024-01-01T00:00:00Z");
|
||||
|
||||
e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMax, "tenant7"));
|
||||
e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMax, "tenant7"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedDateTime`, it must be before `2023-12-31T23:59:59Z`");
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedDateTime`. Cause: it must be before `2023-12-31T23:59:59Z`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -290,15 +291,15 @@ public class InputsTest {
|
||||
void inputValidatedDurationBadValue() {
|
||||
HashMap<String, Object> mapMin = new HashMap<>(inputs);
|
||||
mapMin.put("validatedDuration", "PT1S");
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMin, "tenant8"));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedDuration`, It must be more than `PT10S`, but received `PT1S`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMin, "tenant8"));
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedDuration`. Cause: It must be more than `PT10S`");
|
||||
|
||||
HashMap<String, Object> mapMax = new HashMap<>(inputs);
|
||||
mapMax.put("validatedDuration", "PT30S");
|
||||
|
||||
e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMax, "tenant8"));
|
||||
e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMax, "tenant8"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedDuration`, It must be less than `PT20S`, but received `PT30S`");
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedDuration`. Cause: It must be less than `PT20S`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -306,15 +307,15 @@ public class InputsTest {
|
||||
void inputValidatedFloatBadValue() {
|
||||
HashMap<String, Object> mapMin = new HashMap<>(inputs);
|
||||
mapMin.put("validatedFloat", "0.01");
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMin, "tenant9"));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedFloat`, it must be more than `0.1`, but received `0.01`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMin, "tenant9"));
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedFloat`. Cause: it must be more than `0.1`");
|
||||
|
||||
HashMap<String, Object> mapMax = new HashMap<>(inputs);
|
||||
mapMax.put("validatedFloat", "1.01");
|
||||
|
||||
e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMax, "tenant9"));
|
||||
e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMax, "tenant9"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedFloat`, it must be less than `0.5`, but received `1.01`");
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedFloat`. Cause: it must be less than `0.5`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -322,15 +323,15 @@ public class InputsTest {
|
||||
void inputValidatedTimeBadValue() {
|
||||
HashMap<String, Object> mapMin = new HashMap<>(inputs);
|
||||
mapMin.put("validatedTime", "00:00:01");
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMin, "tenant10"));
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedTime`, it must be after `01:00`, but received `00:00:01`");
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMin, "tenant10"));
|
||||
assertThat(e.getMessage()).contains( "Invalid value for input `validatedTime`. Cause: it must be after `01:00`");
|
||||
|
||||
HashMap<String, Object> mapMax = new HashMap<>(inputs);
|
||||
mapMax.put("validatedTime", "14:00:00");
|
||||
|
||||
e = assertThrows(ConstraintViolationException.class, () -> typedInputs(mapMax, "tenant10"));
|
||||
e = assertThrows(InputOutputValidationException.class, () -> typedInputs(mapMax, "tenant10"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `validatedTime`, it must be before `11:59:59`, but received `14:00:00`");
|
||||
assertThat(e.getMessage()).contains("Invalid value for input `validatedTime`. Cause: it must be before `11:59:59`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -339,9 +340,9 @@ public class InputsTest {
|
||||
HashMap<String, Object> map = new HashMap<>(inputs);
|
||||
map.put("uri", "http:/bla");
|
||||
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(map, "tenant11"));
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(map, "tenant11"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `uri`, Expected `URI` but received `http:/bla`, but received `http:/bla`");
|
||||
assertThat(e.getMessage()).contains( "Invalid value for input `uri`. Cause: Invalid URI format." );
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -350,9 +351,9 @@ public class InputsTest {
|
||||
HashMap<String, Object> map = new HashMap<>(inputs);
|
||||
map.put("enum", "INVALID");
|
||||
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(map, "tenant12"));
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(map, "tenant12"));
|
||||
|
||||
assertThat(e.getMessage()).isEqualTo("enum: Invalid input for `enum`, it must match the values `[ENUM_VALUE, OTHER_ONE]`, but received `INVALID`");
|
||||
assertThat(e.getMessage()).isEqualTo("Invalid value for input `enum`. Cause: it must match the values `[ENUM_VALUE, OTHER_ONE]`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -361,9 +362,9 @@ public class InputsTest {
|
||||
HashMap<String, Object> map = new HashMap<>(inputs);
|
||||
map.put("array", "[\"s1\", \"s2\"]");
|
||||
|
||||
ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> typedInputs(map, "tenant13"));
|
||||
InputOutputValidationException e = assertThrows(InputOutputValidationException.class, () -> typedInputs(map, "tenant13"));
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `array`, Unable to parse array element as `INT` on `s1`, but received `[\"s1\", \"s2\"]`");
|
||||
assertThat(e.getMessage()).contains( "Invalid value for input `array`. Cause: Unable to parse array element as `INT` on `s1`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -467,7 +468,20 @@ public class InputsTest {
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat((String) execution.findTaskRunsByTaskId("file").getFirst().getOutputs().get("value")).isEqualTo(file.toString());
|
||||
}
|
||||
@Test
|
||||
@LoadFlows(value = "flows/invalids/inputs-with-multiple-constraint-violations.yaml")
|
||||
void multipleConstraintViolations() {
|
||||
InputOutputValidationException ex = assertThrows(InputOutputValidationException.class, ()-> runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests", "inputs-with-multiple-constraint-violations", null,
|
||||
(f, e) ->flowIO.readExecutionInputs(f, e , Map.of("multi", List.of("F", "H")) )));
|
||||
|
||||
List<String> messages = Arrays.asList(ex.getMessage().split(System.lineSeparator()));
|
||||
|
||||
assertThat(messages).containsExactlyInAnyOrder(
|
||||
"Invalid value for input `multi`. Cause: you can't define both `values` and `options`",
|
||||
"Invalid value for input `multi`. Cause: value `F` doesn't match the values `[A, B, C]`",
|
||||
"Invalid value for input `multi`. Cause: value `H` doesn't match the values `[A, B, C]`"
|
||||
);
|
||||
}
|
||||
private URI createFile() throws IOException {
|
||||
File tempFile = File.createTempFile("file", ".txt");
|
||||
Files.write(tempFile.toPath(), "Hello World".getBytes());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.core.runners;
|
||||
|
||||
import io.kestra.core.exceptions.InputOutputValidationException;
|
||||
import io.kestra.core.junit.annotations.ExecuteFlow;
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
@@ -71,6 +72,6 @@ public class NoEncryptionConfiguredTest implements TestPropertyProvider {
|
||||
.flowId(flow.getId())
|
||||
.build();
|
||||
|
||||
assertThrows(ConstraintViolationException.class, () -> flowIO.readExecutionInputs(flow, execution, InputsTest.inputs));
|
||||
assertThrows(InputOutputValidationException.class, () -> flowIO.readExecutionInputs(flow, execution, InputsTest.inputs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.plugin.core.flow;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import io.kestra.core.exceptions.InputOutputValidationException;
|
||||
import io.kestra.core.junit.annotations.ExecuteFlow;
|
||||
import io.kestra.core.junit.annotations.FlakyTest;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
@@ -328,12 +329,12 @@ public class PauseTest {
|
||||
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.PAUSED);
|
||||
|
||||
ConstraintViolationException e = assertThrows(
|
||||
ConstraintViolationException.class,
|
||||
InputOutputValidationException e = assertThrows(
|
||||
InputOutputValidationException.class,
|
||||
() -> executionService.resume(execution, flow, State.Type.RUNNING, Mono.empty(), Pause.Resumed.now()).block()
|
||||
);
|
||||
|
||||
assertThat(e.getMessage()).contains("Invalid input for `asked`, missing required input, but received `null`");
|
||||
assertThat(e.getMessage()).contains( "Missing required input:asked");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
id: inputs-with-multiple-constraint-violations
|
||||
namespace: io.kestra.tests
|
||||
inputs:
|
||||
- id: multi
|
||||
type: MULTISELECT
|
||||
values:
|
||||
- A
|
||||
- B
|
||||
- C
|
||||
options:
|
||||
- X
|
||||
- Y
|
||||
- Z
|
||||
|
||||
tasks:
|
||||
- id: validMultiSelect
|
||||
type: io.kestra.plugin.core.debug.Return
|
||||
format: "{{inputs.multi}}"
|
||||
10
core/src/test/resources/flows/valids/each-pause.yaml
Normal file
10
core/src/test/resources/flows/valids/each-pause.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
id: each-pause
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: each_task
|
||||
type: io.kestra.plugin.core.flow.ForEach
|
||||
values: '["a", "b"]'
|
||||
tasks:
|
||||
- id: pause
|
||||
type: io.kestra.plugin.core.flow.Pause
|
||||
@@ -0,0 +1,22 @@
|
||||
DROP INDEX logs_execution_id;
|
||||
DROP INDEX logs_execution_id__task_id;
|
||||
DROP INDEX logs_execution_id__taskrun_id;
|
||||
DROP INDEX logs_namespace_flow;
|
||||
|
||||
ALTER table logs drop column "deleted";
|
||||
|
||||
CREATE INDEX IF NOT EXISTS logs_execution_id ON logs ("execution_id");
|
||||
CREATE INDEX IF NOT EXISTS logs_execution_id__task_id ON logs ("execution_id", "task_id");
|
||||
CREATE INDEX IF NOT EXISTS logs_execution_id__taskrun_id ON logs ("execution_id", "taskrun_id");
|
||||
CREATE INDEX IF NOT EXISTS logs_namespace_flow ON logs ("tenant_id", "timestamp", "level", "namespace", "flow_id");
|
||||
|
||||
|
||||
DROP INDEX IF EXISTS metrics_flow_id;
|
||||
DROP INDEX IF EXISTS metrics_execution_id;
|
||||
DROP INDEX IF EXISTS metrics_timestamp;
|
||||
|
||||
ALTER TABLE metrics drop column "deleted";
|
||||
|
||||
CREATE INDEX IF NOT EXISTS metrics_flow_id ON metrics ("tenant_id", "namespace", "flow_id");
|
||||
CREATE INDEX IF NOT EXISTS metrics_execution_id ON metrics ("execution_id");
|
||||
CREATE INDEX IF NOT EXISTS metrics_timestamp ON metrics ("tenant_id", "timestamp");
|
||||
@@ -0,0 +1,22 @@
|
||||
ALTER TABLE logs DROP INDEX ix_execution_id;
|
||||
ALTER TABLE logs DROP INDEX ix_execution_id__task_id;
|
||||
ALTER TABLE logs DROP INDEX ix_execution_id__taskrun_id;
|
||||
ALTER TABLE logs DROP INDEX ix_namespace_flow;
|
||||
|
||||
ALTER table logs drop column `deleted`;
|
||||
|
||||
ALTER TABLE logs ADD INDEX ix_execution_id (`execution_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE logs ADD INDEX ix_execution_id__task_id (`execution_id`, `task_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE logs ADD INDEX ix_execution_id__taskrun_id (`execution_id`, `taskrun_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE logs ADD INDEX ix_namespace_flow (`tenant_id`, `timestamp`, `level`, `namespace`, `flow_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
|
||||
ALTER TABLE metrics DROP INDEX metrics_flow_id;
|
||||
ALTER TABLE metrics DROP INDEX ix_metrics_execution_id;
|
||||
ALTER TABLE metrics DROP INDEX metrics_timestamp;
|
||||
|
||||
ALTER TABLE metrics drop column `deleted`;
|
||||
|
||||
ALTER TABLE metrics ADD INDEX ix_metrics_flow_id (`tenant_id`, `namespace`, `flow_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE metrics ADD INDEX ix_metrics_execution_id (`execution_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE metrics ADD INDEX ix_metrics_timestamp (`tenant_id`, `timestamp`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Indices will be re-created by the next migration
|
||||
DROP INDEX logs_execution_id;
|
||||
DROP INDEX logs_execution_id__task_id;
|
||||
DROP INDEX logs_execution_id__taskrun_id;
|
||||
DROP INDEX logs_namespace_flow;
|
||||
|
||||
ALTER table logs drop column "deleted";
|
||||
|
||||
DROP INDEX IF EXISTS metrics_flow_id;
|
||||
DROP INDEX IF EXISTS metrics_execution_id;
|
||||
DROP INDEX IF EXISTS metrics_timestamp;
|
||||
|
||||
ALTER TABLE metrics drop column "deleted";
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS logs_execution_id ON logs (execution_id);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS logs_execution_id__task_id ON logs (execution_id, task_id);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS logs_execution_id__taskrun_id ON logs (execution_id, taskrun_id);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS logs_namespace_flow ON logs (tenant_id, timestamp, level, namespace, flow_id);
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS metrics_flow_id ON metrics (tenant_id, namespace, flow_id);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS metrics_execution_id ON metrics (execution_id);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS metrics_timestamp ON metrics (tenant_id, timestamp);
|
||||
@@ -15,6 +15,11 @@ flyway:
|
||||
# We must ignore missing migrations as a V6 wrong migration was created and replaced by the V11
|
||||
ignore-migration-patterns: "*:missing,*:future"
|
||||
out-of-order: true
|
||||
properties:
|
||||
flyway:
|
||||
postgresql:
|
||||
transactional:
|
||||
lock: false
|
||||
|
||||
kestra:
|
||||
server-type: STANDALONE
|
||||
|
||||
@@ -257,10 +257,7 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcCrudReposito
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
return context.delete(this.jdbcRepository.getTable())
|
||||
// The deleted field is not used, so ti will always be false.
|
||||
// We add it here to be sure to use the correct index.
|
||||
.where(field("deleted", Boolean.class).eq(false))
|
||||
.and(field("execution_id", String.class).eq(execution.getId()))
|
||||
.where(field("execution_id", String.class).eq(execution.getId()))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -273,10 +270,7 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcCrudReposito
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
return context.delete(this.jdbcRepository.getTable())
|
||||
// The deleted field is not used, so ti will always be false.
|
||||
// We add it here to be sure to use the correct index.
|
||||
.where(field("deleted", Boolean.class).eq(false))
|
||||
.and(field("execution_id", String.class).in(executions.stream().map(Execution::getId).toList()))
|
||||
.where(field("execution_id", String.class).in(executions.stream().map(Execution::getId).toList()))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -496,5 +490,15 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcCrudReposito
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Condition defaultFilter(String tenantId) {
|
||||
return buildTenantCondition(tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Condition defaultFilter() {
|
||||
return DSL.trueCondition();
|
||||
}
|
||||
|
||||
abstract protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType);
|
||||
}
|
||||
|
||||
@@ -185,10 +185,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcCrudRepos
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
return context.delete(this.jdbcRepository.getTable())
|
||||
// The deleted field is not used, so ti will always be false.
|
||||
// We add it here to be sure to use the correct index.
|
||||
.where(field("deleted", Boolean.class).eq(false))
|
||||
.and(field("execution_id", String.class).eq(execution.getId()))
|
||||
.where(field("execution_id", String.class).eq(execution.getId()))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -201,14 +198,21 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcCrudRepos
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
return context.delete(this.jdbcRepository.getTable())
|
||||
// The deleted field is not used, so ti will always be false.
|
||||
// We add it here to be sure to use the correct index.
|
||||
.where(field("deleted", Boolean.class).eq(false))
|
||||
.and(field("execution_id", String.class).in(executions.stream().map(Execution::getId).toList()))
|
||||
.where(field("execution_id", String.class).in(executions.stream().map(Execution::getId).toList()))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Condition defaultFilter(String tenantId) {
|
||||
return buildTenantCondition(tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Condition defaultFilter() {
|
||||
return DSL.trueCondition();
|
||||
}
|
||||
|
||||
private List<String> queryDistinct(String tenantId, Condition condition, String field) {
|
||||
return this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
|
||||
@@ -30,7 +30,7 @@ dependencies {
|
||||
// as Jackson is in the Micronaut BOM, to force its version we need to use enforcedPlatform but it didn't really work, see later :(
|
||||
api enforcedPlatform("com.fasterxml.jackson:jackson-bom:$jacksonVersion")
|
||||
api enforcedPlatform("org.slf4j:slf4j-api:$slf4jVersion")
|
||||
api platform("io.micronaut.platform:micronaut-platform:4.9.4")
|
||||
api platform("io.micronaut.platform:micronaut-platform:4.10.5")
|
||||
api platform("io.qameta.allure:allure-bom:2.32.0")
|
||||
// we define cloud bom here for GCP, Azure and AWS so they are aligned for all plugins that use them (secret, storage, oss and ee plugins)
|
||||
api platform('com.google.cloud:libraries-bom:26.73.0')
|
||||
@@ -38,6 +38,8 @@ dependencies {
|
||||
api platform('software.amazon.awssdk:bom:2.40.10')
|
||||
api platform("dev.langchain4j:langchain4j-bom:$langchain4jVersion")
|
||||
api platform("dev.langchain4j:langchain4j-community-bom:$langchain4jCommunityVersion")
|
||||
// Micronaut 4.10 brings a Jetty version no compatible with the one from Wiremock so we bump it here
|
||||
api platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.1.2")
|
||||
|
||||
constraints {
|
||||
// downgrade to proto 1.3.2-alpha as 1.5.0 needs protobuf 4
|
||||
|
||||
46
ui/package-lock.json
generated
46
ui/package-lock.json
generated
@@ -4220,18 +4220,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.19.0.tgz",
|
||||
"integrity": "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz",
|
||||
"integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.19.0"
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs/node_modules/@shikijs/types": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz",
|
||||
"integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz",
|
||||
"integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
@@ -4258,18 +4258,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.19.0.tgz",
|
||||
"integrity": "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz",
|
||||
"integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.19.0"
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes/node_modules/@shikijs/types": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz",
|
||||
"integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz",
|
||||
"integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
@@ -18914,24 +18914,6 @@
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki/node_modules/@shikijs/langs": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz",
|
||||
"integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki/node_modules/@shikijs/themes": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz",
|
||||
"integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki/node_modules/@shikijs/types": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
import {ref, computed, watch, onMounted, nextTick, useAttrs} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import EnterpriseBadge from "./EnterpriseBadge.vue";
|
||||
import BlueprintDetail from "./flows/blueprints/BlueprintDetail.vue";
|
||||
import BlueprintDetail from "../override/components/flows/blueprints/BlueprintDetail.vue";
|
||||
|
||||
interface Tab {
|
||||
name?: string;
|
||||
|
||||
@@ -37,25 +37,31 @@
|
||||
const setupFlow = async () => {
|
||||
const blueprintId = route.query.blueprintId as string;
|
||||
const blueprintSource = route.query.blueprintSource as BlueprintType;
|
||||
const blueprintSourceYaml = route.query.blueprintSourceYaml as string;
|
||||
const implicitDefaultNamespace = authStore.user.getNamespacesForAction(
|
||||
permission.FLOW,
|
||||
action.CREATE,
|
||||
)[0];
|
||||
let flowYaml = "";
|
||||
const id = getRandomID();
|
||||
const selectedNamespace = (route.query.namespace as string)
|
||||
?? defaultNamespace()
|
||||
?? implicitDefaultNamespace
|
||||
const selectedNamespace = (route.query.namespace as string)
|
||||
?? defaultNamespace()
|
||||
?? implicitDefaultNamespace
|
||||
?? "company.team";
|
||||
|
||||
if (route.query.copy && flowStore.flow) {
|
||||
flowYaml = flowStore.flow.source;
|
||||
} else if (blueprintId && blueprintSource) {
|
||||
} else if (blueprintId && blueprintSourceYaml) {
|
||||
flowYaml = blueprintSourceYaml;
|
||||
} else if(blueprintId && blueprintSource === "community"){
|
||||
flowYaml = await blueprintsStore.getBlueprintSource({
|
||||
type: blueprintSource,
|
||||
kind: "flow",
|
||||
id: blueprintId
|
||||
});
|
||||
} else if (blueprintId) {
|
||||
const flowBlueprint = await blueprintsStore.getFlowBlueprint(blueprintId);
|
||||
flowYaml = flowBlueprint.source;
|
||||
} else {
|
||||
flowYaml = `
|
||||
id: ${id}
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
<script lang="ts">
|
||||
import {ElMessage} from "element-plus";
|
||||
import ValidationError from "../flows/ValidationError.vue";
|
||||
import {toRaw} from "vue";
|
||||
import {markRaw, toRaw} from "vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
import debounce from "lodash/debounce";
|
||||
@@ -336,10 +336,10 @@
|
||||
editingArrayId: null,
|
||||
editableItems: {},
|
||||
// expose icon components to the template so linters and the template can resolve them
|
||||
DeleteOutline,
|
||||
Pencil,
|
||||
Plus,
|
||||
ContentSave
|
||||
DeleteOutline: markRaw(DeleteOutline),
|
||||
Pencil:markRaw(Pencil),
|
||||
Plus:markRaw(Plus),
|
||||
ContentSave:markRaw(ContentSave)
|
||||
};
|
||||
},
|
||||
emits: ["update:modelValue", "update:modelValueNoDefault", "update:checks", "confirm", "validation"],
|
||||
@@ -566,6 +566,7 @@
|
||||
} else {
|
||||
this.$emit("validation", {
|
||||
formData: formData,
|
||||
inputsMetaData: this.inputsMetaData,
|
||||
callback: (response) => {
|
||||
metadataCallback(response);
|
||||
}
|
||||
|
||||
@@ -90,16 +90,16 @@
|
||||
|
||||
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
|
||||
|
||||
import Editor from "../../inputs/Editor.vue";
|
||||
import Markdown from "../../layout/Markdown.vue";
|
||||
import TopNavBar from "../../layout/TopNavBar.vue";
|
||||
import LowCodeEditor from "../../inputs/LowCodeEditor.vue";
|
||||
import CopyToClipboard from "../../layout/CopyToClipboard.vue";
|
||||
import Editor from "../../../../components/inputs/Editor.vue";
|
||||
import Markdown from "../../../../components/layout/Markdown.vue";
|
||||
import TopNavBar from "../../../../components/layout/TopNavBar.vue";
|
||||
import LowCodeEditor from "../../../../components/inputs/LowCodeEditor.vue";
|
||||
import CopyToClipboard from "../../../../components/layout/CopyToClipboard.vue";
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
|
||||
import {useFlowStore} from "../../../stores/flow";
|
||||
import {usePluginsStore} from "../../../stores/plugins";
|
||||
import {useBlueprintsStore} from "../../../stores/blueprints";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
import {usePluginsStore} from "../../../../stores/plugins";
|
||||
import {useBlueprintsStore} from "../../../../stores/blueprints";
|
||||
|
||||
import {canCreate} from "override/composables/blueprintsPermissions";
|
||||
import {parse as parseFlow} from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
@@ -170,8 +170,8 @@
|
||||
} else if (props.kind === "dashboard") {
|
||||
additionalQuery = {
|
||||
name: "home",
|
||||
params: route.params?.tenant === undefined
|
||||
? undefined
|
||||
params: route.params?.tenant === undefined
|
||||
? undefined
|
||||
: JSON.stringify({tenant: route.params.tenant}),
|
||||
};
|
||||
}
|
||||
@@ -35,9 +35,8 @@
|
||||
import {useI18n} from "vue-i18n";
|
||||
import TopNavBar from "../../../../components/layout/TopNavBar.vue";
|
||||
import DottedLayout from "../../../../components/layout/DottedLayout.vue";
|
||||
// @ts-expect-error - Component not typed
|
||||
import BlueprintDetail from "../../../../components/flows/blueprints/BlueprintDetail.vue";
|
||||
import BlueprintsBrowser from "./BlueprintsBrowser.vue";
|
||||
import BlueprintDetail from "../../../../override/components/flows/blueprints/BlueprintDetail.vue";
|
||||
import BlueprintsBrowser from "../../../../override/components/flows/blueprints/BlueprintsBrowser.vue";
|
||||
import DemoBlueprints from "../../../../components/demo/Blueprints.vue";
|
||||
import useRouteContext from "../../../../composables/useRouteContext";
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
</div>
|
||||
|
||||
<div class="action-button">
|
||||
<slot name="buttons" :blueprint="blueprint" />
|
||||
<el-tooltip v-if="embed && !system" trigger="click" content="Copied" placement="left" :autoClose="2000" effect="light">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -77,9 +76,11 @@
|
||||
class="p-2"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-button v-else-if="userCanCreate" type="primary" size="default" @click.prevent.stop="blueprintToEditor(blueprint.id)">
|
||||
{{ $t('use') }}
|
||||
</el-button>
|
||||
<slot name="buttons" :blueprint="{...blueprint, kind: props.blueprintKind, type: props.blueprintType}">
|
||||
<el-button v-if="!embed && userCanCreate" type="primary" size="default" @click.prevent.stop="blueprintToEditor(blueprint.id)">
|
||||
{{ $t('use') }}
|
||||
</el-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ export default [
|
||||
}
|
||||
if(change) {
|
||||
next({
|
||||
...to,
|
||||
...to,
|
||||
query,
|
||||
});
|
||||
return;
|
||||
@@ -79,7 +79,7 @@ export default [
|
||||
|
||||
//Blueprints
|
||||
{name: "blueprints", path: "/:tenant?/blueprints/:kind/:tab", component: () => import("override/components/flows/blueprints/Blueprints.vue"), props: true},
|
||||
{name: "blueprints/view", path: "/:tenant?/blueprints/:kind/:tab/:blueprintId", component: () => import("../components/flows/blueprints/BlueprintDetail.vue"), props: true},
|
||||
{name: "blueprints/view", path: "/:tenant?/blueprints/:kind/:tab/:blueprintId", component: () => import("../override/components/flows/blueprints/BlueprintDetail.vue"), props: true},
|
||||
|
||||
//Documentation
|
||||
{name: "plugins/list", path: "/:tenant?/plugins", component: () => import("../components/plugins/Plugin.vue")},
|
||||
|
||||
@@ -24,6 +24,31 @@ interface Blueprint {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface TemplateArgument {
|
||||
id: string,
|
||||
displayName: string,
|
||||
type: string,
|
||||
itemType?: string,
|
||||
required: boolean,
|
||||
defaults?: any
|
||||
}
|
||||
|
||||
export interface BlueprintTemplate {
|
||||
source: string;
|
||||
templateArguments: Record<string, TemplateArgument>;
|
||||
}
|
||||
|
||||
export interface FlowBlueprint {
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
includedTasks?: string[],
|
||||
tags?: string[],
|
||||
source: string,
|
||||
publishedAt?: string,
|
||||
template?: BlueprintTemplate
|
||||
}
|
||||
|
||||
const API_URL = "https://api.kestra.io/v1";
|
||||
const VALIDATE = {validateStatus: (status: number) => status === 200 || status === 401};
|
||||
|
||||
@@ -95,6 +120,54 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getFlowBlueprint = async (id: string): Promise<FlowBlueprint> => {
|
||||
const url = `${apiUrl()}/blueprints/flow/${id}`;
|
||||
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (response.data?.id) {
|
||||
trackBlueprintSelection(response.data.id);
|
||||
}
|
||||
|
||||
blueprint.value = response.data;
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createFlowBlueprint = async (toCreate: {source: string, title: string, description: string, tags: string[]}): Promise<FlowBlueprint> => {
|
||||
const url = `${apiUrl()}/blueprints/flows`;
|
||||
const body = {
|
||||
...toCreate
|
||||
}
|
||||
const response = await axios.post(url, body);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateFlowBlueprint = async (id: string, toUpdate: {source: string, title: string, description: string, tags: string[]}) :Promise<FlowBlueprint> => {
|
||||
const url = `${apiUrl()}/blueprints/flows/${id}`;
|
||||
const body = {
|
||||
...toUpdate
|
||||
}
|
||||
const response = await axios.put(url, body);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const deleteFlowBlueprint = async (idToDelete: string) => {
|
||||
const url = `${apiUrl()}/blueprints/flows/${idToDelete}`;
|
||||
await axios.delete(url);
|
||||
};
|
||||
|
||||
const useFlowBlueprintTemplate = async (id: string, inputs: Record<string, object>): Promise<{generatedFlowSource: string}> => {
|
||||
const url = `${apiUrl()}/blueprints/flows/${id}/use-template`;
|
||||
const body = {
|
||||
templateArgumentsInputs: inputs
|
||||
}
|
||||
const response = await axios.post(url, body);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return {
|
||||
blueprint,
|
||||
blueprints,
|
||||
@@ -106,5 +179,10 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
|
||||
getBlueprintSource,
|
||||
getBlueprintGraph,
|
||||
getBlueprintTags,
|
||||
useFlowBlueprintTemplate,
|
||||
getFlowBlueprint,
|
||||
createFlowBlueprint,
|
||||
updateFlowBlueprint,
|
||||
deleteFlowBlueprint,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "Beenden",
|
||||
"kill only parents": "Nur Eltern beenden",
|
||||
"kill parents and subflow": "Eltern und Subflows beenden",
|
||||
"kill only parents": "Nur aktuellen beenden",
|
||||
"kill parents and subflow": "Aktuelle und Subflows beenden",
|
||||
"killed confirm": "Sind Sie sicher, dass Sie die Ausführung <code>{id}</code> beenden möchten?",
|
||||
"killed done": "Ausführung ist zum Beenden eingereiht",
|
||||
"kv": {
|
||||
|
||||
@@ -231,8 +231,8 @@
|
||||
"mark as": "Mark as <code>{status}</code>",
|
||||
"unqueue as": "Unqueue as <code>{status}</code>",
|
||||
"kill": "Kill",
|
||||
"kill parents and subflow": "Kill parents and subflows",
|
||||
"kill only parents": "Kill parents only",
|
||||
"kill parents and subflow": "Kill current and subflows",
|
||||
"kill only parents": "Kill current only",
|
||||
"killed confirm": "Are you sure you want to kill execution <code>{id}</code>?",
|
||||
"killed done": "Execution is queued for killing",
|
||||
"resume": "Resume",
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "Terminar",
|
||||
"kill only parents": "Terminar solo padres",
|
||||
"kill parents and subflow": "Terminar padres y subflows",
|
||||
"kill only parents": "Matar solo el actual",
|
||||
"kill parents and subflow": "Terminar el flujo actual y los subflows",
|
||||
"killed confirm": "¿Estás seguro de terminar la ejecución <code>{id}</code>?",
|
||||
"killed done": "La ejecución está en cola para ser terminada",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Clé",
|
||||
"kill": "Arrêter",
|
||||
"kill only parents": "Arrêter uniquement les exécutions parents",
|
||||
"kill parents and subflow": "Arrêter les exécutions parents et enfants",
|
||||
"kill only parents": "Tuer uniquement le courant",
|
||||
"kill parents and subflow": "Tuer le flow actuel et les subflows",
|
||||
"killed confirm": "Êtes-vous sur de vouloir arrêter l'exécution <code>{id}</code> ?",
|
||||
"killed done": "L'exécution est en attente d'arrêt",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "केस्ट्रा",
|
||||
"key": "Key",
|
||||
"kill": "समाप्त करें",
|
||||
"kill only parents": "केवल parents समाप्त करें",
|
||||
"kill parents and subflow": "parents और subflows समाप्त करें",
|
||||
"kill only parents": "केवल वर्तमान को समाप्त करें",
|
||||
"kill parents and subflow": "वर्तमान और subflows को समाप्त करें",
|
||||
"killed confirm": "क्या आप वाकई निष्पादन <code>{id}</code> को समाप्त करना चाहते हैं?",
|
||||
"killed done": "निष्पादन समाप्त करने के लिए कतारबद्ध है",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "Termina",
|
||||
"kill only parents": "Termina solo genitori",
|
||||
"kill parents and subflow": "Termina genitori e subflows",
|
||||
"kill only parents": "Uccidi solo corrente",
|
||||
"kill parents and subflow": "Uccidi il flow corrente e i subflow",
|
||||
"killed confirm": "Sei sicuro di voler terminare l'esecuzione <code>{id}</code>?",
|
||||
"killed done": "L'esecuzione è in coda per la terminazione",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "キル",
|
||||
"kill only parents": "親のみをキル",
|
||||
"kill parents and subflow": "親とsubflowsをキル",
|
||||
"kill only parents": "現在のもののみを終了",
|
||||
"kill parents and subflow": "現在のflowとsubflowを終了",
|
||||
"killed confirm": "実行<code>{id}</code>をキルしてもよろしいですか?",
|
||||
"killed done": "実行はキルのためにキューに入れられました",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "종료",
|
||||
"kill only parents": "부모만 종료",
|
||||
"kill parents and subflow": "부모 및 subflows 종료",
|
||||
"kill only parents": "현재 것만 종료",
|
||||
"kill parents and subflow": "현재 및 subflow 종료",
|
||||
"killed confirm": "실행 <code>{id}</code>을(를) 종료하시겠습니까?",
|
||||
"killed done": "종료 중입니다.",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Klucz",
|
||||
"kill": "Zakończ",
|
||||
"kill only parents": "Zakończ tylko rodziców",
|
||||
"kill parents and subflow": "Zakończ rodziców i subflows",
|
||||
"kill only parents": "Zabij tylko bieżący",
|
||||
"kill parents and subflow": "Zatrzymaj bieżący flow i subflow",
|
||||
"killed confirm": "Czy na pewno chcesz zakończyć wykonanie <code>{id}</code>?",
|
||||
"killed done": "Wykonanie jest w kolejce do zakończenia",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "Matar",
|
||||
"kill only parents": "Matar apenas pais",
|
||||
"kill parents and subflow": "Matar pais e subflows",
|
||||
"kill only parents": "Matar apenas o atual",
|
||||
"kill parents and subflow": "Matar o flow atual e subflows",
|
||||
"killed confirm": "Tem certeza de que deseja matar a execução <code>{id}</code>?",
|
||||
"killed done": "Execução está na fila para ser morta",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Key",
|
||||
"kill": "Matar",
|
||||
"kill only parents": "Matar apenas pais",
|
||||
"kill parents and subflow": "Matar pais e subflows",
|
||||
"kill only parents": "Matar apenas o atual",
|
||||
"kill parents and subflow": "Matar o flow atual e subflows",
|
||||
"killed confirm": "Tem certeza de que deseja matar a execução <code>{id}</code>?",
|
||||
"killed done": "Execução está na fila para ser morta",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "Ключ",
|
||||
"kill": "Убить",
|
||||
"kill only parents": "Убить только родителей",
|
||||
"kill parents and subflow": "Убить родителей и подпроцессы",
|
||||
"kill only parents": "Убить только текущий",
|
||||
"kill parents and subflow": "Прервать текущий и subflows",
|
||||
"killed confirm": "Вы уверены, что хотите убить выполнение <code>{id}</code>?",
|
||||
"killed done": "Выполнение поставлено в очередь на убийство",
|
||||
"kv": {
|
||||
|
||||
@@ -1027,8 +1027,8 @@
|
||||
"kestra": "Kestra",
|
||||
"key": "键",
|
||||
"kill": "终止",
|
||||
"kill only parents": "仅终止父流程",
|
||||
"kill parents and subflow": "终止父流程和子流程",
|
||||
"kill only parents": "仅终止当前",
|
||||
"kill parents and subflow": "终止当前和subflows",
|
||||
"killed confirm": "确定要终止执行 <code>{id}</code> 吗?",
|
||||
"killed done": "执行已排队终止",
|
||||
"kv": {
|
||||
|
||||
@@ -19,6 +19,10 @@ dependencies {
|
||||
implementation "io.micronaut:micronaut-management"
|
||||
implementation "io.micronaut:micronaut-http-client"
|
||||
implementation "io.micronaut:micronaut-http-server-netty"
|
||||
// See https://github.com/netty-contrib/codec-multipart/pull/25
|
||||
// See https://github.com/kestra-io/kestra/issues/9743
|
||||
// There is an issue on Netty multipart content decoding that is fixed in 5.x, this library will bring the same fix for 4.x
|
||||
implementation("io.netty.contrib:netty-codec-multipart-vintage:1.0.0.Alpha1")
|
||||
implementation "io.micronaut.cache:micronaut-cache-core"
|
||||
implementation "io.micronaut.cache:micronaut-cache-caffeine"
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ public class ErrorController {
|
||||
public HttpResponse<JsonError> error(HttpRequest<?> request, JsonParseException e) {
|
||||
return jsonError(request, e, HttpStatus.UNPROCESSABLE_ENTITY, "Invalid json");
|
||||
}
|
||||
@Error(global = true)
|
||||
public HttpResponse<JsonError> error(HttpRequest<?> request, InputOutputValidationException e) {
|
||||
return jsonError(request, e, HttpStatus.UNPROCESSABLE_ENTITY, "Invalid entity");
|
||||
}
|
||||
|
||||
@Error(global = true)
|
||||
public HttpResponse<JsonError> error(HttpRequest<?> request, ConversionErrorException e) {
|
||||
|
||||
@@ -2670,7 +2670,12 @@ public class ExecutionController {
|
||||
) {
|
||||
}
|
||||
|
||||
public static ApiValidateExecutionInputsResponse of(String id, String namespace, List<Check> checks, List<InputAndValue> inputs) {
|
||||
public static ApiValidateExecutionInputsResponse of(
|
||||
String id,
|
||||
String namespace,
|
||||
List<Check> checks,
|
||||
List<InputAndValue> inputs
|
||||
) {
|
||||
return new ApiValidateExecutionInputsResponse(
|
||||
id,
|
||||
namespace,
|
||||
@@ -2679,14 +2684,21 @@ public class ExecutionController {
|
||||
it.value(),
|
||||
it.enabled(),
|
||||
it.isDefault(),
|
||||
Optional.ofNullable(it.exception()).map(exception ->
|
||||
exception.getConstraintViolations()
|
||||
.stream()
|
||||
.map(cv -> new ApiInputError(cv.getMessage()))
|
||||
// Map the Set<InputOutputValidationException> to ApiInputError
|
||||
Optional.ofNullable(it.exceptions())
|
||||
.map(exSet -> exSet.stream()
|
||||
.map(e -> new ApiInputError(e.getMessage()))
|
||||
.toList()
|
||||
).orElse(List.of())
|
||||
)
|
||||
.orElse(List.of())
|
||||
)).toList(),
|
||||
checks.stream().map(check -> new ApiCheckFailure(check.getMessage(), check.getStyle(), check.getBehavior())).toList()
|
||||
checks.stream()
|
||||
.map(check -> new ApiCheckFailure(
|
||||
check.getMessage(),
|
||||
check.getStyle(),
|
||||
check.getBehavior()
|
||||
))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class DashboardControllerTest {
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
|
||||
charts:
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
@@ -140,7 +140,7 @@ class DashboardControllerTest {
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
|
||||
charts:
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
@@ -195,7 +195,7 @@ class DashboardControllerTest {
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
|
||||
charts:
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
@@ -246,7 +246,7 @@ class DashboardControllerTest {
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
|
||||
charts:
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
@@ -330,7 +330,7 @@ class DashboardControllerTest {
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
|
||||
charts:
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
@@ -378,7 +378,6 @@ class DashboardControllerTest {
|
||||
.namespace(fakeNamespace)
|
||||
.level(Level.INFO)
|
||||
.attemptNumber(1)
|
||||
.deleted(false)
|
||||
.executionId(fakeExecutionId)
|
||||
.tenantId(MAIN_TENANT)
|
||||
.executionKind(ExecutionKind.NORMAL)
|
||||
@@ -447,7 +446,6 @@ class DashboardControllerTest {
|
||||
.namespace(fakeNamespace)
|
||||
.level(Level.INFO)
|
||||
.attemptNumber(1)
|
||||
.deleted(false)
|
||||
.executionId(fakeExecutionId)
|
||||
.tenantId(MAIN_TENANT)
|
||||
.executionKind(ExecutionKind.NORMAL)
|
||||
|
||||
@@ -242,7 +242,7 @@ class ExecutionControllerRunnerTest {
|
||||
String response = e.getResponse().getBody(String.class).orElseThrow();
|
||||
|
||||
assertThat(response).contains("Invalid entity");
|
||||
assertThat(response).contains("Invalid input for `validatedString`");
|
||||
assertThat(response).contains("Invalid value for input `validatedString`");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1071,7 +1071,7 @@ class ExecutionControllerRunnerTest {
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE)
|
||||
));
|
||||
assertThat(exception.getStatus().getCode()).isEqualTo(422);
|
||||
assertThat(exception.getMessage()).isEqualTo("Invalid entity: asked: Invalid input for `asked`, missing required input, but received `null`");
|
||||
assertThat(exception.getMessage()).isEqualTo("Invalid entity: Missing required input:asked");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -57,7 +57,6 @@ public class NamespaceControllerTest {
|
||||
);
|
||||
|
||||
assertThat(namespace.getId()).isEqualTo("my.ns");
|
||||
assertThat(namespace.isDeleted()).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
Reference in New Issue
Block a user