Skip to content

Commit

Permalink
feat(SpEL): implement to configure the limit of characters for SpEL e…
Browse files Browse the repository at this point in the history
…xpressions

Spring Expression Lanuage (SpEL) has a default limit of 10,000 characters. Springframework provides the feature to configure the limit. This feature allows to configure the limit of characters for SpEL expressions.

Approach:
In order to use an expression with characters more than the given default limit, require to follow either of the below approaches:
1. For Springframework >=5.3.28 and <6.1.3, by setting `maximumExpressionLength` field while instantiating the custom `SpelParserConfiguration` class.
spring-projects/spring-framework#30380
spring-projects/spring-framework#30446

2. For Springframework >=6.1.3, by setting a JVM system property or Spring property named `spring.context.expression.maxLength` to the maximum expression length needed by your application.
spring-projects/spring-framework#31952
spring-projects/spring-framework@7855986

Spinnaker supports spring boot 2.7.18, that brings springframework 5.3.31 [https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/dependency-versions.html#appendix.dependency-versions.propertie9]. So first approach need to be implemented along with spinnaker enhancement to expose the `maximumExpressionLength` field.
  • Loading branch information
j-sandy committed Jul 1, 2024
1 parent 9bcd1fe commit 792111f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator;
import com.netflix.spinnaker.fiat.shared.FiatStatus;
import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService;
import com.netflix.spinnaker.kork.expressions.config.ExpressionProperties;
import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand All @@ -34,7 +35,8 @@
@EnableConfigurationProperties({
FiatClientConfigurationProperties.class,
PipelineCacheConfigurationProperties.class,
QuietPeriodIndicatorConfigurationProperties.class
QuietPeriodIndicatorConfigurationProperties.class,
ExpressionProperties.class
})
public class PipelineTriggerConfiguration {
private OkHttpClientProvider clientProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact;
import com.netflix.spinnaker.kork.expressions.ExpressionEvaluationSummary;
import com.netflix.spinnaker.kork.expressions.ExpressionTransform;
import com.netflix.spinnaker.kork.expressions.config.ExpressionProperties;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
Expand All @@ -26,11 +29,18 @@
@Component
public class ExpectedArtifactExpressionEvaluationPostProcessor implements PipelinePostProcessor {
private final ObjectMapper mapper;
private final ExpressionParser parser = new SpelExpressionParser();
private final ExpressionParser parser;
private final ParserContext parserContext = new TemplateParserContext("${", "}");

public ExpectedArtifactExpressionEvaluationPostProcessor(ObjectMapper mapper) {
public ExpectedArtifactExpressionEvaluationPostProcessor(
ObjectMapper mapper, @Autowired ExpressionProperties expressionProperties) {
this.mapper = mapper;
parser =
new SpelExpressionParser(
expressionProperties.getMaxExpressionLength() > 0
? new SpelParserConfiguration(
null, null, false, false, 0, expressionProperties.getMaxExpressionLength())
: new SpelParserConfiguration());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import com.netflix.spinnaker.echo.model.Trigger
import com.netflix.spinnaker.echo.test.RetrofitStubs
import com.netflix.spinnaker.kork.artifacts.model.Artifact
import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact
import com.netflix.spinnaker.kork.expressions.config.ExpressionProperties
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Subject

class ExpectedArtifactExpressionEvaluationPostProcessorSpec extends Specification implements RetrofitStubs {
@Subject
def artifactPostProcessor = new ExpectedArtifactExpressionEvaluationPostProcessor(EchoObjectMapper.getInstance())
def artifactPostProcessor = new ExpectedArtifactExpressionEvaluationPostProcessor(EchoObjectMapper.getInstance(), new ExpressionProperties())

@Shared
def trigger = Trigger.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2024 OpsMx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.echo.pipelinetriggers.postprocessors;

import static org.junit.jupiter.api.Assertions.assertTrue;

import com.netflix.spinnaker.echo.jackson.EchoObjectMapper;
import com.netflix.spinnaker.echo.model.Pipeline;
import com.netflix.spinnaker.echo.model.Trigger;
import com.netflix.spinnaker.kork.artifacts.model.Artifact;
import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact;
import com.netflix.spinnaker.kork.expressions.config.ExpressionProperties;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;

@ContextConfiguration(classes = ExpectedArtifactExpressionLengthTest.class)
@SpringBootTest
@EnableConfigurationProperties(ExpressionProperties.class)
@TestPropertySource(properties = {"expression.max-expression-length=11000"})
class ExpectedArtifactExpressionLengthTest {
@Autowired private ExpressionProperties expressionProperties;

@Test
void customExpressionLength() {
String expression = String.format("%s", repeat("T", 10900));

ExpectedArtifactExpressionEvaluationPostProcessor artifactPostProcessor =
new ExpectedArtifactExpressionEvaluationPostProcessor(
EchoObjectMapper.getInstance(), expressionProperties);

Trigger trigger =
Trigger.builder()
.enabled(true)
.type("jenkins")
.master("master")
.job(expression)
.buildNumber(100)
.build();

ExpectedArtifact artifact =
ExpectedArtifact.builder()
.matchArtifact(
Artifact.builder()
.name(
"group:artifact:${trigger['job'] == '"
+ expression
+ "' ? 'expr-worked' : 'expr-not-worked'}")
.version("${trigger['buildNumber']}")
.type("maven/file")
.build())
.id("testId")
.build();

Pipeline inputPipeline =
Pipeline.builder()
.application("application")
.name("name")
.id(Integer.toString(new AtomicInteger(1).getAndIncrement()))
.trigger(trigger)
.expectedArtifacts(List.of(artifact))
.build()
.withTrigger(trigger);

Pipeline outputPipeline = artifactPostProcessor.processPipeline(inputPipeline);

Artifact evaluatedArtifact = outputPipeline.getExpectedArtifacts().get(0).getMatchArtifact();

assertTrue(evaluatedArtifact.getName().equalsIgnoreCase("group:artifact:expr-worked"));
}

private String repeat(String str, int count) {
String res = "";
for (int i = 0; i < count; i++) {
res += str;
}
return res;
}
}

0 comments on commit 792111f

Please sign in to comment.