Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): Allow registering custom SpEL expression functions #2701

Merged
merged 2 commits into from
Mar 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
import com.netflix.spinnaker.orca.pipeline.DefaultStageDefinitionBuilderFactory;
import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder;
import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilderFactory;
import com.netflix.spinnaker.orca.pipeline.expressions.ExpressionFunctionProvider;
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository;
import com.netflix.spinnaker.orca.pipeline.util.ContextFunctionConfiguration;
import com.netflix.spinnaker.orca.pipeline.util.ContextParameterProcessor;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
Expand All @@ -51,13 +51,15 @@
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import rx.Notification;
import rx.Scheduler;
import rx.schedulers.Schedulers;

import java.time.Clock;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static java.time.temporal.ChronoUnit.MINUTES;
import static org.springframework.context.annotation.AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME;
Expand Down Expand Up @@ -122,9 +124,13 @@ UserConfiguredUrlRestrictions userConfiguredUrlRestrictions(UserConfiguredUrlRes

@Bean
public ContextFunctionConfiguration contextFunctionConfiguration(UserConfiguredUrlRestrictions userConfiguredUrlRestrictions,
@Value("${spelEvaluator:v2}")
String spelEvaluator) {
return new ContextFunctionConfiguration(userConfiguredUrlRestrictions, spelEvaluator);
Optional<List<ExpressionFunctionProvider>> expressionFunctionProviders,
@Value("${spelEvaluator:v2}") String spelEvaluator) {
return new ContextFunctionConfiguration(
userConfiguredUrlRestrictions,
expressionFunctionProviders.orElse(Collections.emptyList()),
spelEvaluator
);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2019 Netflix, 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.orca.pipeline.expressions

interface ExpressionFunctionProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kotlin in your src/main/java?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what we've done with other orca-core kotlin code, yeah. Kinda like Java in our groovy src. :\

val namespace: String?
val functions: Collection<FunctionDefinition>

data class FunctionDefinition(
val name: String,
val parameters: List<FunctionParameter>
)

data class FunctionParameter(
val type: Class<*>,
val name: String,
val description: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,7 @@

package com.netflix.spinnaker.orca.pipeline.expressions;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.netflix.spinnaker.orca.ExecutionStatus;
import com.netflix.spinnaker.orca.pipeline.expressions.whitelisting.FilteredMethodResolver;
import com.netflix.spinnaker.orca.pipeline.expressions.whitelisting.FilteredPropertyAccessor;
import com.netflix.spinnaker.orca.pipeline.expressions.whitelisting.MapPropertyAccessor;
Expand All @@ -41,6 +31,16 @@
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;

import static java.lang.String.format;

/**
* Provides utility support for SPEL integration
* Supports registering SPEL functions, ACLs to classes (via whitelisting)
Expand All @@ -52,8 +52,6 @@ public class ExpressionsSupport {
private static AtomicReference<ContextFunctionConfiguration> helperFunctionConfigurationAtomicReference = new AtomicReference<>();
private static Map<String, List<Class<?>>> registeredHelperFunctions = new HashMap<>();

private static List<String> DEPLOY_STAGE_NAMES = Arrays.asList("deploy", "createServerGroup", "cloneServerGroup", "rollingPush");

ExpressionsSupport(ContextFunctionConfiguration contextFunctionConfiguration) {
helperFunctionConfigurationAtomicReference.set(contextFunctionConfiguration);
}
Expand Down Expand Up @@ -102,8 +100,25 @@ public static StandardEvaluationContext newEvaluationContext(Object rootObject,
registerFunction(evaluationContext, "stageExists", Object.class, String.class);
registerFunction(evaluationContext, "judgment", Object.class, String.class);
registerFunction(evaluationContext, "judgement", Object.class, String.class);
registerFunction(evaluationContext, "deployedServerGroups", Object.class, String[].class);
registerFunction(evaluationContext, "manifestLabelValue", Object.class, String.class, String.class, String.class);

ContextFunctionConfiguration contextFunctionConfiguration = helperFunctionConfigurationAtomicReference.get();
for (ExpressionFunctionProvider p : contextFunctionConfiguration.getExpressionFunctionProviders()) {
for (ExpressionFunctionProvider.FunctionDefinition function : p.getFunctions()) {
String namespacedFunctionName = function.getName();
if (p.getNamespace() != null) {
namespacedFunctionName = format("%s_%s", p.getNamespace(), namespacedFunctionName);
}
Class[] functionTypes = function.getParameters()
.stream()
.map(ExpressionFunctionProvider.FunctionParameter::getType)
.toArray(Class[]::new);
LOGGER.info("Registering Expression Function: {}({})", namespacedFunctionName, functionTypes);

evaluationContext.registerFunction(
namespacedFunctionName, p.getClass().getDeclaredMethod(function.getName(), functionTypes)
);
}
}
}
} catch (NoSuchMethodException e) {
// Indicates a function was not properly registered. This should not happen. Please fix the faulty function
Expand Down Expand Up @@ -185,7 +200,7 @@ static String toJson(Object o) {

return converted;
} catch (Exception e) {
throw new SpelHelperFunctionException(String.format("#toJson(%s) failed", o.toString()), e);
throw new SpelHelperFunctionException(format("#toJson(%s) failed", o.toString()), e);
}
}

Expand All @@ -199,7 +214,7 @@ static String fromUrl(String url) {
URL u = helperFunctionConfigurationAtomicReference.get().getUrlRestrictions().validateURI(url).toURL();
return HttpClientUtils.httpGetAsString(u.toString());
} catch (Exception e) {
throw new SpelHelperFunctionException(String.format("#from(%s) failed", url), e);
throw new SpelHelperFunctionException(format("#from(%s) failed", url), e);
}
}

Expand All @@ -216,7 +231,7 @@ static Object readJson(String text) {

return mapper.readValue(text, Map.class);
} catch (Exception e) {
throw new SpelHelperFunctionException(String.format("#readJson(%s) failed", text), e);
throw new SpelHelperFunctionException(format("#readJson(%s) failed", text), e);
}
}

Expand All @@ -238,7 +253,7 @@ static Map propertiesFromUrl(String url) {
try {
return readProperties(fromUrl(url));
} catch (Exception e) {
throw new SpelHelperFunctionException(String.format("#propertiesFromUrl(%s) failed", url), e);
throw new SpelHelperFunctionException(format("#propertiesFromUrl(%s) failed", url), e);
}
}

Expand Down Expand Up @@ -271,12 +286,12 @@ static Object stage(Object obj, String id) {
.findFirst()
.orElseThrow(
() -> new SpelHelperFunctionException(
String.format("Unable to locate [%s] using #stage(%s) in execution %s", id, id, execution.getId())
format("Unable to locate [%s] using #stage(%s) in execution %s", id, id, execution.getId())
)
);
}

throw new SpelHelperFunctionException(String.format("Invalid first param to #stage(%s). must be an execution", id));
throw new SpelHelperFunctionException(format("Invalid first param to #stage(%s). must be an execution", id));
}

/**
Expand All @@ -293,7 +308,7 @@ static boolean stageExists(Object obj, String id) {
.anyMatch(i -> id != null && (id.equals(i.getName()) || id.equals(i.getId())));
}

throw new SpelHelperFunctionException(String.format("Invalid first param to #stage(%s). must be an execution", id));
throw new SpelHelperFunctionException(format("Invalid first param to #stage(%s). must be an execution", id));
}

/**
Expand All @@ -311,7 +326,7 @@ static String judgment(Object obj, String id) {
.findFirst()
.orElseThrow(
() -> new SpelHelperFunctionException(
String.format("Unable to locate manual Judgment stage [%s] using #judgment(%s) in execution %s. " +
format("Unable to locate manual Judgment stage [%s] using #judgment(%s) in execution %s. " +
"Stage doesn't exist or doesn't contain judgmentInput in its context ",
id, id, execution.getId()
)
Expand All @@ -322,112 +337,10 @@ static String judgment(Object obj, String id) {
}

throw new SpelHelperFunctionException(
String.format("Invalid first param to #judgment(%s). must be an execution", id)
format("Invalid first param to #judgment(%s). must be an execution", id)
);
}

static List<Map<String, Object>> deployedServerGroups(Object obj, String...id) {
if (obj instanceof Execution) {
List<Map<String, Object>> deployedServerGroups = new ArrayList<>();
((Execution) obj).getStages()
.stream()
.filter(matchesDeployedStage(id))
.forEach(stage -> {
String region = (String) stage.getContext().get("region");
if (region == null) {
Map<String, Object> availabilityZones = (Map<String, Object>) stage.getContext().get("availabilityZones");
if (availabilityZones != null) {
region = availabilityZones.keySet().iterator().next();
}
}

if (region != null) {
Map<String, Object> deployDetails = new HashMap<>();
deployDetails.put("account", stage.getContext().get("account"));
deployDetails.put("capacity", stage.getContext().get("capacity"));
deployDetails.put("parentStage", stage.getContext().get("parentStage"));
deployDetails.put("region", region);
List<Map> existingDetails = (List<Map>) stage.getContext().get("deploymentDetails");
if (existingDetails != null) {
existingDetails
.stream()
.filter(d -> deployDetails.get("region").equals(d.get("region")))
.forEach(deployDetails::putAll);
}

List<Map> serverGroups = (List<Map>) ((Map) stage.getContext().get("deploy.server.groups")).get(region);
if (serverGroups != null) {
deployDetails.put("serverGroup", serverGroups.get(0));
}

deployedServerGroups.add(deployDetails);
}
});

return deployedServerGroups;
}

throw new IllegalArgumentException("An execution is required for this function");
}

/**
* Gets value of given label key in manifest of given kind deployed by stage of given name
* @param obj #root.execution
* @param stageName the name of a `deployManifest` stage to find
* @param kind the kind of manifest to find
* @param labelKey the key of the label to find
* @return the label value
*/
static String manifestLabelValue(Object obj, String stageName, String kind, String labelKey) {
if (!(obj instanceof Execution)) {
throw new IllegalArgumentException("An execution is required for this function");
}

List<String> validKinds = Arrays.asList("Deployment", "ReplicaSet");
if (!validKinds.contains(kind)) {
throw new IllegalArgumentException("Only Deployments and ReplicaSets are valid kinds for this function");
}

if (labelKey == null) {
throw new IllegalArgumentException("A labelKey is required for this function");
}

Optional<Stage> stage = ((Execution) obj).getStages()
.stream()
.filter(s -> s.getName().equals(stageName) && s.getType().equals("deployManifest") && s.getStatus() == ExecutionStatus.SUCCEEDED)
.findFirst();

if (!stage.isPresent()) {
throw new SpelHelperFunctionException("A valid Deploy Manifest stage name is required for this function");
}

List<Map> manifests = (List<Map>) stage.get().getContext().get("manifests");

if (manifests == null || manifests.size() == 0) {
throw new SpelHelperFunctionException("No manifest could be found in the context of the specified stage");
}

Optional<Map> manifestOpt = manifests.stream()
.filter(m -> m.get("kind").equals(kind))
.findFirst();

if (!manifestOpt.isPresent()) {
throw new SpelHelperFunctionException(String.format("No manifest of kind %s could be found on the context of the specified stage", kind));
}

Map manifest = manifestOpt.get();
String labelPath = String.format("$.spec.template.metadata.labels.%s", labelKey);
String labelValue;

try {
labelValue = JsonPath.read(manifest, labelPath);
} catch (PathNotFoundException e) {
throw new SpelHelperFunctionException("No label of specified key found on matching manifest spec.template.metadata.labels");
}

return labelValue;
}

/**
* Alias to judgment
*/
Expand All @@ -438,17 +351,4 @@ static String judgement(Object obj, String id) {
private static Predicate<Stage> isManualStageWithManualInput(String id) {
return i -> (id != null && id.equals(i.getName())) && (i.getContext() != null && i.getType().equals("manualJudgment") && i.getContext().get("judgmentInput") != null);
}

private static Predicate<Stage> matchesDeployedStage(String ...id) {
List<String> idsOrNames = Arrays.asList(id);
if (!idsOrNames.isEmpty()){
return stage -> DEPLOY_STAGE_NAMES.contains(stage.getType()) &&
stage.getContext().containsKey("deploy.server.groups") &&
stage.getStatus() == ExecutionStatus.SUCCEEDED &&
(idsOrNames.contains(stage.getName()) || idsOrNames.contains(stage.getId()));
} else {
return stage -> DEPLOY_STAGE_NAMES.contains(stage.getType()) &&
stage.getContext().containsKey("deploy.server.groups") && stage.getStatus() == ExecutionStatus.SUCCEEDED;
}
}
}
Loading