diff --git a/kork-expressions/kork-expressions.gradle b/kork-expressions/kork-expressions.gradle index 67903e659..f1bf50d00 100644 --- a/kork-expressions/kork-expressions.gradle +++ b/kork-expressions/kork-expressions.gradle @@ -18,6 +18,7 @@ dependencies { testImplementation project(":kork-artifacts") testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.junit.jupiter:junit-jupiter-params" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" testRuntimeOnly "org.slf4j:slf4j-simple" } diff --git a/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupport.java b/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupport.java index ad7e1347d..3392ec73a 100644 --- a/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupport.java +++ b/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupport.java @@ -27,6 +27,7 @@ import com.netflix.spinnaker.kork.expressions.allowlist.MapPropertyAccessor; import com.netflix.spinnaker.kork.expressions.allowlist.ReturnTypeRestrictor; import com.netflix.spinnaker.kork.expressions.config.ExpressionProperties; +import com.netflix.spinnaker.kork.expressions.functions.ArtifactStoreFunctions; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -96,7 +97,9 @@ public ExpressionsSupport( expressionFunctionProviders = new ArrayList<>( Arrays.asList( - new JsonExpressionFunctionProvider(), new StringExpressionFunctionProvider())); + new ArtifactStoreFunctions(), + new JsonExpressionFunctionProvider(), + new StringExpressionFunctionProvider())); if (extraExpressionFunctionProviders != null) { expressionFunctionProviders.addAll(extraExpressionFunctionProviders); @@ -166,6 +169,7 @@ private StandardEvaluationContext createEvaluationContext( evaluationContext.setTypeLocator(new AllowListTypeLocator()); evaluationContext.setTypeConverter( new ArtifactUriToReferenceConverter(ArtifactStore.getInstance())); + evaluationContext.setMethodResolvers( Collections.singletonList(new FilteredMethodResolver(returnTypeRestrictor))); evaluationContext.setPropertyAccessors( diff --git a/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/functions/ArtifactStoreFunctions.java b/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/functions/ArtifactStoreFunctions.java new file mode 100644 index 000000000..827f8977a --- /dev/null +++ b/kork-expressions/src/main/java/com/netflix/spinnaker/kork/expressions/functions/ArtifactStoreFunctions.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Apple 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.kork.expressions.functions; + +import com.netflix.spinnaker.kork.api.expressions.ExpressionFunctionProvider; +import org.jetbrains.annotations.Nullable; + +/** Houses all SpEL related functions that deal specifically with the artifact store */ +public class ArtifactStoreFunctions implements ExpressionFunctionProvider { + @Nullable + @Override + public String getNamespace() { + return null; + } + + /** + * Used to return the original based64 reference that was stored by the artifact store. It's + * important to note that utilizing this SpEL function will balloon the context, e.g. increasing + * the size of the execution context. + * + *

{@code ${ #fetchReference(#stage('Bake Manifest').context.artifacts[0].reference) }} + */ + public static String fetchReference(String ref) { + // We do not have to do anything here since the artifact URI converter will + // convert any references to the original stored reference + // + // One caveat with this function is that if there is nothing to retrieve, + // the original string will just be returned + return ref; + } + + @Override + public Functions getFunctions() { + return new Functions( + new FunctionDefinition( + "fetchReference", + "Retrieve artifact reference", + new FunctionParameter( + String.class, + "ref", + "Will return the associated artifact's base64 reference back."))); + } +} diff --git a/kork-expressions/src/test/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupportTest.java b/kork-expressions/src/test/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupportTest.java index 19024ec83..1ae7dcd22 100644 --- a/kork-expressions/src/test/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupportTest.java +++ b/kork-expressions/src/test/java/com/netflix/spinnaker/kork/expressions/ExpressionsSupportTest.java @@ -17,7 +17,6 @@ package com.netflix.spinnaker.kork.expressions; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -32,7 +31,12 @@ import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import java.util.stream.Stream; +import lombok.SneakyThrows; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateParserContext; @@ -107,29 +111,36 @@ public void testToJsonWhenComposedExpressionAndEvaluationContext() { assertThat(evaluated).isEqualTo("{\"json_file\":\"${#toJson(#doNotEval(file_json))}\"}"); } - @Test - public void artifactReferenceInSpEL() { + @ParameterizedTest + @MethodSource("artifactReferenceArgs") + public void artifactReferenceInSpEL(String reference, String expected, String expr) { MockArtifactStore artifactStore = new MockArtifactStore(); ArtifactStore.setInstance(artifactStore); ExpressionProperties expressionProperties = new ExpressionProperties(); - String expectedValue = "Hello world"; - artifactStore.cache.put("ref://app/sha", expectedValue); - String expr = "${#fromBase64(\"ref://app/sha\")}"; + artifactStore.cache.put("ref://app/sha", reference); Map testContext = - Collections.singletonMap( - "artifactReference", Collections.singletonMap("artifactReference", expr)); + Collections.singletonMap("artifact", Artifact.builder().reference("ref://app/sha")); ExpressionsSupport expressionsSupport = new ExpressionsSupport(null, expressionProperties); StandardEvaluationContext evaluationContext = - expressionsSupport.buildEvaluationContext( - new ExpressionTransformTest.Pipeline(new ExpressionTransformTest.Trigger(123)), true); + expressionsSupport.buildEvaluationContext(testContext, true); String evaluated = new ExpressionTransform(parserContext, parser, Function.identity()) .transformString(expr, evaluationContext, new ExpressionEvaluationSummary()); - assertThat(evaluated).isEqualTo(expectedValue); + assertThat(evaluated).isEqualTo(expected); + } + + @SneakyThrows + public static Stream artifactReferenceArgs() { + return Stream.of( + Arguments.of("Hello world", "Hello world", "${#fromBase64(\"ref://app/sha\")}"), + Arguments.of( + "Hello world", + Base64.getEncoder().encodeToString("Hello world".getBytes()), + "${#fetchReference(artifact.reference)}")); } @Test