Skip to content

Commit

Permalink
feat(core): validate tasks and triggers with dynamic properties
Browse files Browse the repository at this point in the history
  • Loading branch information
loicmathieu committed Jan 8, 2025
1 parent 6890062 commit b03ab07
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 32 deletions.
15 changes: 6 additions & 9 deletions core/src/main/java/io/kestra/core/models/property/Property.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.runners.RunContext;
import io.kestra.core.serializers.JacksonMapper;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.*;

import java.io.IOException;
import java.io.Serial;
Expand Down Expand Up @@ -44,11 +41,6 @@ public class Property<T> {
private String expression;
private T value;

//TODO: Temporary to make the ValueExtractor work, but it's not supposed to say here
public T getValue(){
return this.value;
}

// used only by the deserializer and in tests
@VisibleForTesting
public Property(String expression) {
Expand Down Expand Up @@ -355,6 +347,11 @@ String getExpression() {
return this.expression;
}

// used only by the value extractor
T getValue() {
return value;
}

static class PropertyDeserializer extends StdDeserializer<Property<?>> {
@Serial
private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package io.kestra.core.validations.extractors;
package io.kestra.core.models.property;

import io.kestra.core.models.property.Property;
import io.micronaut.context.annotation.Context;
import jakarta.validation.valueextraction.ExtractedValue;
import jakarta.validation.valueextraction.ValueExtractor;

/**
* Jakarta Bean Validation value extractor for a Property.<br>
*
* This is used by the @{@link io.kestra.core.validations.factory.CustomValidatorFactoryProvider}.
*/
@Context
public class PropertyValueExtractor implements ValueExtractor<Property<@ExtractedValue ?>> {

@Override
public void extractValues(Property<?> originalValue, ValueReceiver receiver) {
// this will disable validation at save time but enable it at runtime when the value would be populated
receiver.value( null, originalValue.getValue());
}
}
28 changes: 28 additions & 0 deletions core/src/main/java/io/kestra/core/runners/DefaultRunContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.executions.AbstractMetricEntry;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.services.KVStoreService;
import io.kestra.core.storages.Storage;
import io.kestra.core.storages.StorageInterface;
Expand Down Expand Up @@ -59,6 +61,10 @@ public class DefaultRunContext extends RunContext {
private Map<String, Object> pluginConfiguration;
private List<String> secretInputs;

// those are only used to validate dynamic properties inside the RunContextProperty
private Task task;
private AbstractTrigger trigger;

private final AtomicBoolean isInitialized = new AtomicBoolean(false);


Expand Down Expand Up @@ -102,6 +108,16 @@ public ApplicationContext getApplicationContext() {
return applicationContext;
}

@JsonIgnore
Task getTask() {
return task;
}

@JsonIgnore
AbstractTrigger getTrigger() {
return trigger;
}

void init(final ApplicationContext applicationContext) {
if (isInitialized.compareAndSet(false, true)) {
this.applicationContext = applicationContext;
Expand Down Expand Up @@ -150,6 +166,14 @@ void setTriggerExecutionId(final String triggerExecutionId) {
this.triggerExecutionId = triggerExecutionId;
}

void setTask(final Task task) {
this.task = task;
}

void setTrigger(final AbstractTrigger trigger) {
this.trigger = trigger;
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -521,6 +545,8 @@ public static class Builder {
private RunContextLogger logger;
private KVStoreService kvStoreService;
private List<String> secretInputs;
private Task task;
private AbstractTrigger trigger;

/**
* Builds the new {@link DefaultRunContext} object.
Expand All @@ -541,6 +567,8 @@ public DefaultRunContext build() {
context.triggerExecutionId = triggerExecutionId;
context.kvStoreService = kvStoreService;
context.secretInputs = secretInputs;
context.task = task;
context.trigger = trigger;
return context;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public RunContext of(Flow flow, Task task, Execution execution, TaskRun taskRun,
.build(runContextLogger))
.withKvStoreService(kvStoreService)
.withSecretInputs(secretInputsFromFlow(flow))
.withTask(task)
.build();
}

Expand All @@ -131,6 +132,7 @@ public RunContext of(Flow flow, AbstractTrigger trigger) {
.build(runContextLogger)
)
.withSecretInputs(secretInputsFromFlow(flow))
.withTrigger(trigger)
.build();
}

Expand All @@ -148,6 +150,11 @@ public RunContext of(final Flow flow, final Map<String, Object> variables) {

@VisibleForTesting
public RunContext of(final Map<String, Object> variables) {
return of((Task) null, variables);
}

@VisibleForTesting
public RunContext of(final Task task, final Map<String, Object> variables) {
RunContextLogger runContextLogger = new RunContextLogger();
return newBuilder()
.withLogger(runContextLogger)
Expand Down Expand Up @@ -177,6 +184,7 @@ public String getNamespace() {
flowService
))
.withVariables(variables)
.withTask(task)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ private DefaultRunContext forWorker(final DefaultRunContext runContext,
runContext.setPluginConfiguration(pluginConfigurations.getConfigurationByPluginTypeOrAliases(task.getType(), task.getClass()));
runContext.setStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forTask(taskRun), storageInterface, flowService));
runContext.setLogger(runContextLogger);
runContext.setTask(task);

return runContext;
}
Expand Down Expand Up @@ -226,6 +227,7 @@ public DefaultRunContext forScheduler(final DefaultRunContext runContext,
runContext.setStorage(storage);
runContext.setPluginConfiguration(pluginConfigurations.getConfigurationByPluginTypeOrAliases(trigger.getType(), trigger.getClass()));
runContext.setTriggerExecutionId(triggerExecutionId);
runContext.setTrigger(trigger);

return runContext;
}
Expand Down
93 changes: 81 additions & 12 deletions core/src/main/java/io/kestra/core/runners/RunContextProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static io.kestra.core.utils.Rethrow.throwFunction;

Expand All @@ -14,86 +21,148 @@
*
* @param <T>
*/
@Slf4j
public class RunContextProperty<T> {
private final Property<T> property;
private final RunContext runContext;
private final Task task;
private final AbstractTrigger trigger;

RunContextProperty(Property<T> property, RunContext runContext) {
this.property = property;
this.runContext = runContext;
this.task = ((DefaultRunContext) runContext).getTask();
this.trigger = ((DefaultRunContext) runContext).getTrigger();
}

/**
* Render a property then convert it to its target type.<br>
* Validate a bean using Jakarta Bean Validation.
* TODO this seems useful as a general util so maybe move it to the RunContext
*/
public <U> void validate(U bean) {
Validator validator = ((DefaultRunContext) runContext).getApplicationContext().getBean(Validator.class);
Set<ConstraintViolation<U>> violations = validator.validate(bean);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}

private void validate() {
if (task != null) {
validate(task);
} else if (trigger != null) {
validate(trigger);
} else {
// this should never happen, but if it happens, we may lower to debug
log.warn("Unable to do validation: no task or trigger found");
}
}

/**
* Render a property then convert it to its target type and validate it.<br>
*
* Validation will only occur if the runContext has been created with a Task or an AbstractTrigger.<br>
*
* 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.
*/
public Optional<T> as(Class<T> clazz) throws IllegalVariableEvaluationException {
return Optional.ofNullable(this.property)
var as = Optional.ofNullable(this.property)
.map(throwFunction(prop -> Property.as(prop, this.runContext, clazz)));

validate();
return as;
}

/**
* Render a property with additional variables, then convert it to its target type.<br>
* Render a property with additional variables, then convert it to its target type and validate it.<br>
*
* Validation will only occur if the runContext has been created with a Task or an AbstractTrigger.<br>
*
* 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.
*/
public Optional<T> as(Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return Optional.ofNullable(this.property)
var as = Optional.ofNullable(this.property)
.map(throwFunction(prop -> Property.as(prop, this.runContext, clazz, variables)));

validate();
return as;
}

/**
* Render a property then convert it as a list of target type.
* Render a property then convert it as a list of target type and validate it.
* Null properties will return an empty list.<br>
*
* Validation will only occur if the runContext has been created with a Task or an AbstractTrigger.<br>
*
* 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.
*/
@SuppressWarnings("unchecked")
public <I> T asList(Class<I> itemClazz) throws IllegalVariableEvaluationException {
return Optional.ofNullable(this.property)
var as = Optional.ofNullable(this.property)
.map(throwFunction(prop -> Property.asList(prop, this.runContext, itemClazz)))
.orElse((T) Collections.emptyList());

validate();
return as;
}

/**
* Render a property with additional variables, then convert it as a list of target type.
* Render a property with additional variables, then convert it as a list of target type and validate it.
* Null properties will return an empty list.<br>
*
* Validation will only occur if the runContext has been created with a Task or an AbstractTrigger.<br>
*
* 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.
*/
@SuppressWarnings("unchecked")
public <I> T asList(Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return Optional.ofNullable(this.property)
var as = Optional.ofNullable(this.property)
.map(throwFunction(prop -> Property.asList(prop, this.runContext, itemClazz, variables)))
.orElse((T) Collections.emptyList());

validate();
return as;
}

/**
* Render a property then convert it as a map of target types.
* Render a property then convert it as a map of target types and validate it.
* Null properties will return an empty map.<br>
*
* Validation will only occur if the runContext has been created with a Task or an AbstractTrigger.<br>
*
* 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.
*/
@SuppressWarnings("unchecked")
public <K,V> T asMap(Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
return Optional.ofNullable(this.property)
var as = Optional.ofNullable(this.property)
.map(throwFunction(prop -> Property.asMap(prop, this.runContext, keyClass, valueClass)))
.orElse((T) Collections.emptyMap());

validate();
return as;
}

/**
* Render a property with additional variables, then convert it as a map of target types.
* Render a property with additional variables, then convert it as a map of target types and validate it.
* Null properties will return an empty map.<br>
*
* Validation will only occur if the runContext has been created with a Task or an AbstractTrigger.<br>
*
* 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.
*/
@SuppressWarnings("unchecked")
public <K,V> T asMap(Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return Optional.ofNullable(this.property)
var as = Optional.ofNullable(this.property)
.map(throwFunction(prop -> Property.asMap(prop, this.runContext, keyClass, valueClass, variables)))
.orElse((T) Collections.emptyMap());

validate();
return as;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.kestra.core.validations.factory;

import io.kestra.core.validations.extractors.PropertyValueExtractor;
import io.kestra.core.models.property.PropertyValueExtractor;
import io.micronaut.configuration.hibernate.validator.ValidatorFactoryProvider;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Replaces;
Expand All @@ -10,16 +10,12 @@
import io.micronaut.core.annotation.TypeHint;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.Configuration;
import jakarta.validation.ConstraintValidatorFactory;
import jakarta.validation.MessageInterpolator;
import jakarta.validation.ParameterNameProvider;
import jakarta.validation.TraversableResolver;
import jakarta.validation.Validation;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.*;

import java.util.Map;
import java.util.Optional;
import java.util.Properties;

import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;

Expand Down
Loading

0 comments on commit b03ab07

Please sign in to comment.