/*************************************************************************
* Copyright 2009-2013 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.cloudformation.template;
import com.eucalyptus.cloudformation.*;
import com.eucalyptus.cloudformation.entity.StackEntity;
import com.eucalyptus.cloudformation.resources.ResourceInfo;
import com.eucalyptus.cloudformation.resources.ResourceResolverManager;
import com.eucalyptus.cloudformation.template.dependencies.CyclicDependencyException;
import com.eucalyptus.cloudformation.template.dependencies.DependencyManager;
import com.eucalyptus.util.Json;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.PatternSyntaxException;
/**
* Created by ethomas on 12/10/13.
*/
public class TemplateParser {
private static final String NO_ECHO_PARAMETER_VALUE = "****";
private static final Logger LOG = Logger.getLogger(TemplateParser.class);
public TemplateParser() {
}
public enum Capabilities {
CAPABILITY_IAM,
CAPABILITY_NAMED_IAM
}
private enum TemplateSection {
Metadata,
AWSTemplateFormatVersion,
Description,
Parameters,
Mappings,
Conditions,
Resources,
Outputs
};
private enum ParameterKey {
Type,
Default,
NoEcho,
AllowedValues,
AllowedPattern,
MaxLength,
MinLength,
MaxValue,
MinValue,
Description,
ConstraintDescription
};
private enum ResourceKey {
Type,
Properties,
DeletionPolicy,
Description,
DependsOn,
Metadata,
UpdatePolicy,
CreationPolicy,
Condition
}
private enum DeletionPolicyValues {
Delete,
Retain,
Snapshot
}
private enum OutputKey {
Description,
Condition,
Value
};
public static final String AWS_ACCOUNT_ID = "AWS::AccountId";
public static final String AWS_NOTIFICATION_ARNS = "AWS::NotificationARNs";
public static final String AWS_NO_VALUE = "AWS::NoValue";
public static final String AWS_REGION = "AWS::Region";
public static final String AWS_STACK_ID = "AWS::StackId";
public static final String AWS_STACK_NAME = "AWS::StackName";
private static final String DEFAULT_TEMPLATE_VERSION = "2010-09-09";
private static final String[] validTemplateVersions = new String[] {DEFAULT_TEMPLATE_VERSION};
public Template parse(String templateBody, List<Parameter> userParameters, List<String> capabilities, PseudoParameterValues pseudoParameterValues, String effectiveUserId, boolean enforceStrictResourceProperties) throws CloudFormationException {
Template template = new Template();
template.setResourceInfoMap(Maps.<String, ResourceInfo>newLinkedHashMap());
JsonNode templateJsonNode;
try {
templateJsonNode = Json.parse( templateBody );
} catch (IOException ex) {
throw new ValidationErrorException(ex.getMessage());
}
if (!templateJsonNode.isObject()) {
throw new ValidationErrorException("Template body is not a JSON object");
}
template.setTemplateBody(templateBody);
addPseudoParameters(template, pseudoParameterValues);
buildResourceMap(template, templateJsonNode, enforceStrictResourceProperties);
parseValidTopLevelKeys(templateJsonNode);
parseVersion(template, templateJsonNode);
parseMetadata(template, templateJsonNode);
parseDescription(template, templateJsonNode);
parseMappings(template, templateJsonNode);
ParameterParser.parseParameters(template, templateJsonNode, userParameters, false);
parseConditions(template, templateJsonNode, false, effectiveUserId);
parseResources(template, templateJsonNode, false);
Map<String, JsonNode> resourcePropertyMap = createResourcePropertiesMap(templateJsonNode);
Set<String> requiredCapabilities = Sets.newHashSet();
capabilities = addIAMCapabilityIfNamed(capabilities);
for (Map.Entry<String, ResourceInfo> resourceInfoEntry: template.getResourceInfoMap().entrySet()) {
String resourceName = resourceInfoEntry.getKey();
ResourceInfo resourceInfo = resourceInfoEntry.getValue();
JsonNode resourcePropertiesJson = resourcePropertyMap.get(resourceName);
String resourceType = resourceInfo.getType();
Collection resourceRequiredCapabilities = resourceInfo.getRequiredCapabilities(resourcePropertiesJson);
if (resourceRequiredCapabilities != null && !resourceRequiredCapabilities.isEmpty()) {
requiredCapabilities.addAll(resourceRequiredCapabilities);
}
}
requiredCapabilities = consolidateIAMCapabilities(requiredCapabilities);
Set<String> missingRequiredCapabilities = Sets.newLinkedHashSet();
if (!requiredCapabilities.isEmpty()) {
for (String requiredCapability:requiredCapabilities) {
if (capabilities == null || !capabilities.contains(requiredCapability)) {
missingRequiredCapabilities.add(requiredCapability);
}
}
}
if (!missingRequiredCapabilities.isEmpty()) {
throw new InsufficientCapabilitiesException("Required capabilities:" + missingRequiredCapabilities);
}
parseOutputs(template, templateJsonNode);
return template;
}
private List<String> addIAMCapabilityIfNamed(List<String> capabilities) {
List<String> newCapabilities = Lists.newArrayList();
if (capabilities != null) {
newCapabilities.addAll(capabilities);
}
// CAPABILITY_NAMED_IAM also gives CAPABILITY_IAM capability
if (newCapabilities.contains(Capabilities.CAPABILITY_NAMED_IAM.toString()) &&
!newCapabilities.contains(Capabilities.CAPABILITY_IAM.toString())) {
newCapabilities.add(Capabilities.CAPABILITY_IAM.toString());
}
return newCapabilities;
}
private void parseMetadata(Template template, JsonNode templateJsonNode) throws CloudFormationException {
JsonNode metadataResourcesJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Metadata.toString());
template.setMetadataJSON(JsonHelper.getStringFromJsonNode(metadataResourcesJsonNode));
}
public ValidateTemplateResult validateTemplate(String templateBody, List<Parameter> userParameters, PseudoParameterValues pseudoParameterValues, String effectiveUserId, boolean enforceStrictResourceProperties) throws CloudFormationException {
GetTemplateSummaryResult getTemplateSummaryResult = getTemplateSummary(templateBody, userParameters, pseudoParameterValues, effectiveUserId, enforceStrictResourceProperties);
ValidateTemplateResult validateTemplateResult = new ValidateTemplateResult();
validateTemplateResult.setDescription(getTemplateSummaryResult.getDescription());
validateTemplateResult.setCapabilities(getTemplateSummaryResult.getCapabilities());
validateTemplateResult.setCapabilitiesReason(getTemplateSummaryResult.getCapabilitiesReason());
TemplateParameters templateParameters = new TemplateParameters();
if (getTemplateSummaryResult.getParameters() != null && getTemplateSummaryResult.getParameters().getMember() != null) {
templateParameters.setMember(Lists.<TemplateParameter>newArrayList());
for (ParameterDeclaration parameterDeclaration: getTemplateSummaryResult.getParameters().getMember()) {
TemplateParameter templateParameter = new TemplateParameter();
templateParameter.setDefaultValue(parameterDeclaration.getDefaultValue());
templateParameter.setDescription(parameterDeclaration.getDescription());
templateParameter.setNoEcho(parameterDeclaration.getNoEcho());
templateParameter.setParameterKey(parameterDeclaration.getParameterKey());
templateParameters.getMember().add(templateParameter);
}
}
validateTemplateResult.setParameters(templateParameters);
return validateTemplateResult;
}
public GetTemplateSummaryResult getTemplateSummary(String templateBody, List<Parameter> userParameters, PseudoParameterValues pseudoParameterValues, String effectiveUserId, boolean enforceStrictResourceProperties) throws CloudFormationException {
Template template = new Template();
template.setResourceInfoMap(Maps.<String, ResourceInfo>newLinkedHashMap());
JsonNode templateJsonNode;
try {
templateJsonNode = Json.parse(templateBody);
} catch (IOException ex) {
throw new ValidationErrorException(ex.getMessage());
}
if (!templateJsonNode.isObject()) {
throw new ValidationErrorException("Template body is not a JSON object");
}
template.setTemplateBody(templateBody);
addPseudoParameters(template, pseudoParameterValues);
buildResourceMap(template, templateJsonNode, enforceStrictResourceProperties);
parseValidTopLevelKeys(templateJsonNode);
parseVersion(template, templateJsonNode);
parseDescription(template, templateJsonNode);
parseMetadata(template, templateJsonNode);
parseMappings(template, templateJsonNode);
ParameterParser.parseParameters(template, templateJsonNode, userParameters, true);
parseConditions(template, templateJsonNode, true, effectiveUserId);
parseResources(template, templateJsonNode, true);
parseOutputs(template, templateJsonNode);
Set<String> capabilitiesResourceTypes = Sets.newLinkedHashSet();
Set<String> requiredCapabilities = Sets.newLinkedHashSet();
Set<String> resourceTypes = Sets.newLinkedHashSet();
Map<String, JsonNode> resourcePropertyMap = createResourcePropertiesMap(templateJsonNode);
for (Map.Entry<String, ResourceInfo> resourceInfoEntry: template.getResourceInfoMap().entrySet()) {
String resourceName = resourceInfoEntry.getKey();
ResourceInfo resourceInfo = resourceInfoEntry.getValue();
JsonNode resourcePropertiesJson = resourcePropertyMap.get(resourceName);
String resourceType = resourceInfo.getType();
resourceTypes.add(resourceType);
Collection resourceRequiredCapabilities = resourceInfo.getRequiredCapabilities(resourcePropertiesJson);
if (resourceRequiredCapabilities != null && !resourceRequiredCapabilities.isEmpty()) {
requiredCapabilities.addAll(resourceRequiredCapabilities);
capabilitiesResourceTypes.add(resourceType);
}
}
requiredCapabilities = consolidateIAMCapabilities(requiredCapabilities);
GetTemplateSummaryResult getTemplateSummaryResult = new GetTemplateSummaryResult();
getTemplateSummaryResult.setDescription(template.getDescription());
getTemplateSummaryResult.setCapabilities(new ResourceList());
getTemplateSummaryResult.getCapabilities().setMember(Lists.newArrayList(requiredCapabilities));
if (!requiredCapabilities.isEmpty()) {
getTemplateSummaryResult.setCapabilitiesReason("The following resource(s) require capabilities: " + capabilitiesResourceTypes);
}
getTemplateSummaryResult.setParameters(new ParameterDeclarations());
getTemplateSummaryResult.getParameters().setMember(template.getParameterDeclarations());
getTemplateSummaryResult.setMetadata(template.getMetadataJSON());
getTemplateSummaryResult.setResourceTypes(new ResourceList());
getTemplateSummaryResult.getResourceTypes().setMember(Lists.newArrayList(resourceTypes));
return getTemplateSummaryResult;
}
private Set<String> consolidateIAMCapabilities(Set<String> capabilities) {
Set<String> newCapabilities = Sets.newHashSet();
if (capabilities != null) {
newCapabilities.addAll(capabilities);
}
// Hack: CAPABILITY_NAMED_IAM is a stronger condition than CAPABILITY_IAM, so remove the latter requirement if
// the former is found
if (newCapabilities.contains(Capabilities.CAPABILITY_NAMED_IAM.toString())) {
newCapabilities.remove(Capabilities.CAPABILITY_IAM.toString());
}
return newCapabilities;
}
private Map<String, JsonNode> createResourcePropertiesMap(JsonNode templateJsonNode) throws CloudFormationException {
Map<String, JsonNode> map = Maps.newHashMap();
JsonNode resourcesJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Resources.toString());
for (String name: Lists.newArrayList(resourcesJsonNode.fieldNames())) {
map.put(name, resourcesJsonNode.get(name).get(ResourceKey.Properties.toString()));
}
return map;
}
private void addPseudoParameters(Template template, PseudoParameterValues pseudoParameterValues) throws CloudFormationException {
// The reason all of this json wrapping is going around is because evaluating a pseudoparameter needs to result in a json node (as it could be an array or string)
Map<String, String> pseudoParameterMap = template.getPseudoParameterMap();
pseudoParameterMap.put(AWS_ACCOUNT_ID, JsonHelper.getStringFromJsonNode(new TextNode(pseudoParameterValues.getAccountId())));
ArrayNode notificationsArnNode = JsonHelper.createArrayNode( );
if (pseudoParameterValues.getNotificationArns() != null) {
for (String notificationArn: pseudoParameterValues.getNotificationArns()) {
notificationsArnNode.add(notificationArn);
}
}
pseudoParameterMap.put(AWS_NOTIFICATION_ARNS, JsonHelper.getStringFromJsonNode(notificationsArnNode));
ObjectNode noValueNode = JsonHelper.createObjectNode( );
noValueNode.put(FunctionEvaluation.REF_STR, AWS_NO_VALUE);
pseudoParameterMap.put(AWS_NO_VALUE, JsonHelper.getStringFromJsonNode(noValueNode));
pseudoParameterMap.put(AWS_REGION, JsonHelper.getStringFromJsonNode(new TextNode(pseudoParameterValues.getRegion())));
pseudoParameterMap.put(AWS_STACK_ID, JsonHelper.getStringFromJsonNode(new TextNode(pseudoParameterValues.getStackId())));
pseudoParameterMap.put(AWS_STACK_NAME, JsonHelper.getStringFromJsonNode(new TextNode(pseudoParameterValues.getStackName())));
template.setPseudoParameterMap(pseudoParameterMap);
}
private void parseValidTopLevelKeys(JsonNode templateJsonNode) throws CloudFormationException {
Set<String> tempTopLevelKeys = Sets.newHashSet(templateJsonNode.fieldNames());
for (TemplateSection section: TemplateSection.values()) {
tempTopLevelKeys.remove(section.toString());
}
if (!tempTopLevelKeys.isEmpty()) {
throw new ValidationErrorException("Invalid template property or properties " + tempTopLevelKeys);
}
}
private void parseVersion(Template template, JsonNode templateJsonNode) throws CloudFormationException {
String templateFormatVersion = JsonHelper.getString(templateJsonNode,
TemplateSection.AWSTemplateFormatVersion.toString(),
"unsupported value for " + TemplateSection.AWSTemplateFormatVersion + ". No such version.");
if (templateFormatVersion == null) {
template.setTemplateFormatVersion(DEFAULT_TEMPLATE_VERSION);
return;
}
if (!Arrays.asList(validTemplateVersions).contains(templateFormatVersion)) {
throw new ValidationErrorException("Template format error: unsupported value for "
+ TemplateSection.AWSTemplateFormatVersion + ".");
}
template.setTemplateFormatVersion(templateFormatVersion);
}
private void parseDescription(Template template, JsonNode templateJsonNode) throws CloudFormationException {
String description = JsonHelper.getString(templateJsonNode, TemplateSection.Description.toString());
if (description == null) return;
if (description.getBytes().length > Limits.TEMPLATE_DESCRIPTION_MAX_LENGTH_BYTES) {
throw new ValidationErrorException("Template format error: " + TemplateSection.Description + " must "
+ "be no longer than " + Limits.TEMPLATE_DESCRIPTION_MAX_LENGTH_BYTES + " bytes");
}
template.setDescription(description);
}
private void parseMappings(Template template, JsonNode templateJsonNode) throws CloudFormationException {
Map<String, Map<String, Map<String, String>>> mapping = template.getMapping();
JsonNode mappingsJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Mappings.toString());
if (mappingsJsonNode == null) return;
for (String mapName: Lists.newArrayList(mappingsJsonNode.fieldNames())) {
if (mapName.length() > Limits.MAPPING_NAME_MAX_LENGTH_CHARS) {
throw new ValidationErrorException("Mapping name " + mapName + " exceeds the maximum number of allowed characters (" + Limits.MAPPING_NAME_MAX_LENGTH_CHARS + ")");
}
JsonNode mappingJsonNode = JsonHelper.checkObject(mappingsJsonNode, mapName, "Every "
+ TemplateSection.Mappings + " member " + mapName + " must be a map");
for (String mapKey: Lists.newArrayList(mappingJsonNode.fieldNames())) {
if (mapKey.length() > Limits.MAPPING_NAME_MAX_LENGTH_CHARS) {
throw new ValidationErrorException("Mapping key " + mapKey + " exceeds the maximum number of allowed characters (" + Limits.MAPPING_NAME_MAX_LENGTH_CHARS + ")");
}
JsonNode attributesJsonNode = JsonHelper.checkObject(mappingJsonNode, mapKey, "Every "
+ TemplateSection.Mappings + " member " + mapKey + " must be a map");
if (Lists.newArrayList(attributesJsonNode.fieldNames()).size() > Limits.MAX_ATTRIBUTES_PER_MAPPING) {
throw new ValidationErrorException("Mapping with key " + mapKey + " has more than " + Limits.MAX_ATTRIBUTES_PER_MAPPING + ", the max allowed.");
}
for (String attribute: Lists.newArrayList(attributesJsonNode.fieldNames())) {
if (attribute.length() > Limits.MAPPING_NAME_MAX_LENGTH_CHARS) {
throw new ValidationErrorException("Attribute " + attribute + " exceeds the maximum number of allowed characters (" + Limits.MAPPING_NAME_MAX_LENGTH_CHARS + ")");
}
JsonNode valueJsonNode = JsonHelper.checkStringOrArray(attributesJsonNode, attribute, "Every "
+ TemplateSection.Mappings + " attribute must be a String or a List.");
if (!mapping.containsKey(mapName)) {
mapping.put(mapName, Maps.<String, Map<String, String>>newLinkedHashMap());
}
if (!mapping.get(mapName).containsKey(mapKey)) {
mapping.get(mapName).put(mapKey, Maps.<String, String>newLinkedHashMap());
}
mapping.get(mapName).get(mapKey).put(attribute, JsonHelper.getStringFromJsonNode(valueJsonNode));
}
}
}
if (mapping.keySet().size() > Limits.MAX_MAPPINGS_PER_TEMPLATE) {
throw new ValidationErrorException("Mappings exceed maximum allowed of " + Limits.MAX_MAPPINGS_PER_TEMPLATE + " mappings per template");
}
template.setMapping(mapping);
}
static class ParameterParser {
static void parseParameters(Template template, JsonNode templateJsonNode,
List<com.eucalyptus.cloudformation.Parameter> userParameters,
boolean onlyEvaluateTemplate) throws CloudFormationException {
JsonNode parametersJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateParser.TemplateSection.Parameters.toString());
if (parametersJsonNode != null) {
Map<String, String> userParameterMap = Maps.newHashMap();
if (userParameters != null) {
for (com.eucalyptus.cloudformation.Parameter userParameter : userParameters) {
userParameterMap.put(userParameter.getParameterKey(), userParameter.getParameterValue());
}
}
List<Parameter> parameterList = Lists.newArrayList();
for (String parameterKey : Lists.newArrayList(parametersJsonNode.fieldNames())) {
JsonNode parameterJsonNode = JsonHelper.checkObject(parametersJsonNode, parameterKey, "Any "
+ TemplateParser.TemplateSection.Parameters + " member must be a JSON object.");
Parameter parameter = parseParameter(parameterKey, parameterJsonNode, userParameterMap);
parameterList.add(parameter);
}
// construct new objects and check non-existent values
List<String> noValueParameters = Lists.newArrayList();
if (parameterList.size() > Limits.MAX_PARAMETERS_PER_TEMPLATE) {
throw new ValidationErrorException("Too many parameters in the template. Max of " + Limits.MAX_PARAMETERS_PER_TEMPLATE + " allowed.");
}
for (Parameter parameter : parameterList) {
if (parameter.getParameter().getKey().length() > Limits.PARAMETER_NAME_MAX_LENGTH_CHARS) {
throw new ValidationErrorException("Parameter " + parameter.getParameter().getKey() +
" exceeds the maximum parameter name length of " + Limits.PARAMETER_NAME_MAX_LENGTH_CHARS + " characters.");
}
if (parameter.getParameter().getStringValue() != null && parameter.getParameter().getStringValue().getBytes().length > Limits.PARAMETER_VALUE_MAX_LENGTH_BYTES) {
throw new ValidationErrorException("Parameter " + parameter.getParameter().getKey() +
" exceeds the maximum parameter value length of " + Limits.PARAMETER_VALUE_MAX_LENGTH_BYTES + " bytes.");
}
template.getParameters().add(parameter.getParameter());
template.getParameterDeclarations().add(parameter.getParameterDeclaration());
if (parameter.getParameter().getStringValue() == null) {
noValueParameters.add(parameter.getParameter().getKey());
}
}
if (!noValueParameters.isEmpty() && !onlyEvaluateTemplate) {
throw new ValidationErrorException("Parameters: " + noValueParameters + " must have values");
}
// check user supplied values actually match parameters
Set<String> userParamKeys = Sets.newHashSet();
Set<String> templateParamKeys = Sets.newHashSet();
if (userParameterMap != null) {
userParamKeys.addAll(userParameterMap.keySet());
}
if (parametersJsonNode != null) {
templateParamKeys.addAll(Sets.newHashSet(parametersJsonNode.fieldNames()));
}
userParamKeys.removeAll(templateParamKeys);
if (!userParamKeys.isEmpty()) {
throw new ValidationErrorException("Parameters: " + userParamKeys + " do not exist in the template");
}
}
}
private static Parameter parseParameter(String parameterName, JsonNode parameterJsonNode, Map<String, String> userParameterMap) throws CloudFormationException {
validateParameterKeys(parameterJsonNode);
String actualParameterTypeStr = JsonHelper.getString(parameterJsonNode, ParameterKey.Type.toString());
ParameterType actualParameterType = parseType(actualParameterTypeStr);
ParameterType parsedIndividualType = null; // String or Number, or CommaDelimitedList. Other list cases, what the individual elements are.
boolean isList = false;
switch (actualParameterType) {
// intentionally grouping case statements here
case String:
case AWS_EC2_AvailabilityZone_Name:
case AWS_EC2_Image_Id:
case AWS_EC2_Instance_Id:
case AWS_EC2_KeyPair_KeyName:
case AWS_EC2_SecurityGroup_Id:
case AWS_EC2_SecurityGroup_GroupName:
case AWS_EC2_Subnet_Id:
case AWS_EC2_Volume_Id:
case AWS_EC2_VPC_Id:
parsedIndividualType = ParameterType.String;
isList = false;
break;
case Number:
parsedIndividualType = ParameterType.Number;
isList = false;
break;
case List_Number:
parsedIndividualType = ParameterType.Number;
isList = true;
break;
case List_String:
case List_AWS_EC2_AvailabilityZone_Name:
case List_AWS_EC2_Image_Id:
case List_AWS_EC2_Instance_Id:
case List_AWS_EC2_KeyPair_KeyName:
case List_AWS_EC2_SecurityGroup_Id:
case List_AWS_EC2_SecurityGroup_GroupName:
case List_AWS_EC2_Subnet_Id:
case List_AWS_EC2_Volume_Id:
case List_AWS_EC2_VPC_Id:
parsedIndividualType = ParameterType.String;
isList = true;
break;
case CommaDelimitedList: // strangely enough this is grandfathered in AWS. Many parameters which work on List<String> individually do not work here, so this is
// treated separately for AWS compatibility
parsedIndividualType = ParameterType.CommaDelimitedList;
isList = false; // this SHOULD be false, I know it doesn't seem it, but it is a grandfathered case.
break;
default:
throw new ValidationErrorException("Template format error: Unrecognized parameter type: " + actualParameterType +". Valid values are " + Arrays.toString(ParameterType.displayValues()));
}
String[] allowedValues = parseAllowedValues(parameterJsonNode); // type not needed
String allowedPattern = parseAllowedPattern(parameterName, parameterJsonNode, parsedIndividualType);
String constraintDescription = parseConstraintDescription(parameterName, parameterJsonNode); // type not needed
String description = parseDescription(parameterName, parameterJsonNode); // type not needed
Double maxLength = parseMaxLength(parameterName, parameterJsonNode, parsedIndividualType);
Double minLength = parseMinLength(parameterName, parameterJsonNode, parsedIndividualType);
if (maxLength != null && minLength != null && maxLength < minLength) {
throw new ValidationErrorException("Template error: Parameter '" + parameterName + "' " + ParameterKey.MinLength
+ " must be less than " + ParameterKey.MaxLength + ".");
}
Double maxValue = parseMaxValue(parameterName, parameterJsonNode, parsedIndividualType);
Double minValue = parseMinValue(parameterName, parameterJsonNode, parsedIndividualType);
if (maxValue != null && minValue != null && maxValue < minValue) {
throw new ValidationErrorException("Template error: Parameter '" + parameterName + "' " + ParameterKey.MinValue
+ " must be less than " + ParameterKey.MaxValue + ".");
}
List<String> valuesToCheck = Lists.newArrayList();
String defaultValue = JsonHelper.getString(parameterJsonNode, ParameterKey.Default.toString()); // could be null
String userDefinedValue = userParameterMap.get(parameterName); // could be null
if (isList) {
valuesToCheck.addAll(splitAndTrimCSVString(defaultValue));
valuesToCheck.addAll(splitAndTrimCSVString(userDefinedValue));
} else {
valuesToCheck.add(defaultValue);
valuesToCheck.add(userDefinedValue);
}
boolean noEcho = "true".equalsIgnoreCase(JsonHelper.getString(parameterJsonNode, ParameterKey.NoEcho.toString()));
// now check any values that exist
for (String value : valuesToCheck) {
if (value != null) {
checkAllowedValues(parameterName, value, allowedValues, constraintDescription);
}
switch (parsedIndividualType) {
case String:
if (value != null) {
parseStringParameter(parameterName, value, allowedPattern, minLength, maxLength, constraintDescription);
}
break;
case Number:
if (value != null) {
parseNumberParameter(parameterName, value, minValue, maxValue, constraintDescription);
}
break;
case CommaDelimitedList:
break; // currently nothing to check here
default:
throw new ValidationErrorException("Template format error: Unrecognized parameter type: " + parsedIndividualType);
}
}
String stringValue = null;
if (defaultValue != null) stringValue = defaultValue;
if (userDefinedValue != null) stringValue = userDefinedValue;
JsonNode jsonValueNode = null;
if (stringValue != null) {
if (isList || actualParameterType == ParameterType.CommaDelimitedList) {
ArrayNode arrayNode = JsonHelper.createArrayNode( );
for (String s: splitAndTrimCSVString(stringValue)) {
arrayNode.add(s);
}
jsonValueNode = arrayNode;
} else {
jsonValueNode = new TextNode(stringValue);
}
}
StackEntity.Parameter parameter = new StackEntity.Parameter();
parameter.setKey(parameterName);
parameter.setNoEcho(noEcho);
parameter.setJsonValue(JsonHelper.getStringFromJsonNode(jsonValueNode));
parameter.setStringValue(stringValue);
ParameterDeclaration parameterDeclaration = new ParameterDeclaration();
parameterDeclaration.setDescription(description);
parameterDeclaration.setDefaultValue(defaultValue);
parameterDeclaration.setNoEcho(noEcho);
parameterDeclaration.setParameterKey(parameterName);
if (allowedValues!=null) {
ParameterConstraints parameterConstraints = new ParameterConstraints();
ResourceList allowedValuesResourceList = new ResourceList();
allowedValuesResourceList.setMember(Lists.newArrayList(allowedValues));
parameterConstraints.setAllowedValues(allowedValuesResourceList);
parameterDeclaration.setParameterConstraints(parameterConstraints);
}
parameterDeclaration.setParameterType(actualParameterTypeStr);
return new Parameter(parameter, parameterDeclaration);
}
private static Collection<String> splitAndTrimCSVString(String stringValue) {
List<String> retVal = Lists.newArrayList();
if (stringValue == null) {
retVal.add(stringValue);
} else {
// this is a special case. StringTokenizers will often ignore ,, cases
StringBuilder currVal = new StringBuilder();
char[] cArray = stringValue.toCharArray();
for (int i = 0; i < cArray.length; i++) {
if (cArray[i] == ',') {
retVal.add(currVal.toString().trim());
currVal.setLength(0); // reset
} else {
currVal.append(cArray[i]);
}
}
retVal.add(currVal.toString().trim());
}
return retVal;
}
private static void parseNumberParameter(String parameterName, String value, Double minValue, Double maxValue, String constraintDescription) throws ValidationErrorException {
String constraintErrorMessage = null;
Double valueDouble = null;
try {
valueDouble = Double.parseDouble(value);
} catch (NumberFormatException ex) {
constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must be a number";
}
if (constraintErrorMessage == null && minValue != null && minValue > valueDouble) {
constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must be a number not less than " + minValue;
}
if (constraintErrorMessage == null && maxValue != null && maxValue < valueDouble) {
constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must be a number not greater than " + maxValue;
}
if (constraintErrorMessage != null && constraintDescription != null) {
constraintErrorMessage = "Parameter '" + parameterName + "' failed to satisfy constraint: " + constraintDescription;
}
if (constraintErrorMessage != null) {
throw new ValidationErrorException(constraintErrorMessage);
}
}
private static void parseStringParameter(String parameterName, String value, String allowedPattern, Double minLength, Double maxLength, String constraintDescription) throws ValidationErrorException {
String constraintErrorMessage = null;
if (minLength != null && minLength > value.length()) {
constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must contain at least " + minLength + " characters";
}
if (constraintErrorMessage == null && maxLength != null && maxLength < value.length()) {
constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must contain at most " + maxLength + " characters";
}
if (constraintErrorMessage == null && allowedPattern != null) {
try {
if (!value.matches(allowedPattern)) {
constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must match pattern " + allowedPattern;
}
} catch (PatternSyntaxException ex) {
// not a constraint violation
throw new ValidationErrorException("Parameter '" + parameterName + "' " + ParameterKey.AllowedPattern
+ " must be a valid regular expression.");
}
}
if (constraintErrorMessage != null && constraintDescription != null) {
constraintErrorMessage = "Parameter '" + parameterName + "' failed to satisfy constraint: " + constraintDescription;
}
if (constraintErrorMessage != null) {
throw new ValidationErrorException(constraintErrorMessage);
}
}
private static void checkAllowedValues(String parameterName,
String value,
String[] allowedValues,
String constraintDescription) throws ValidationErrorException {
if (allowedValues != null) {
if (!Arrays.asList(allowedValues).contains(value)) {
String constraintErrorMessage = "Template error: Parameter '" + parameterName + "' must be one of " + ParameterKey.AllowedValues;
if (constraintDescription != null) {
constraintErrorMessage = "Parameter '" + parameterName + "' failed to satisfy constraint: " + constraintDescription;
}
throw new ValidationErrorException(constraintErrorMessage);
}
}
}
private static Double parseMinValue(String parameterName, JsonNode parameterJsonNode, ParameterType type) throws CloudFormationException {
Double minValue = JsonHelper.getDouble(parameterJsonNode, ParameterKey.MinValue.toString()); // null ok
checkParameterType(minValue, parameterName, ParameterKey.MinValue, type, ParameterType.Number);
return minValue;
}
private static Double parseMaxValue(String parameterName, JsonNode parameterJsonNode, ParameterType type) throws CloudFormationException {
Double maxValue = JsonHelper.getDouble(parameterJsonNode, ParameterKey.MaxValue.toString()); // null ok
checkParameterType(maxValue, parameterName, ParameterKey.MaxValue, type, ParameterType.Number);
return maxValue;
}
private static Double parseMinLength(String parameterName, JsonNode parameterJsonNode, ParameterType type) throws CloudFormationException {
Double minLength = JsonHelper.getDouble(parameterJsonNode, ParameterKey.MinLength.toString()); // null ok
checkParameterType(minLength, parameterName, ParameterKey.MinLength, type, ParameterType.String);
return minLength;
}
private static Double parseMaxLength(String parameterName, JsonNode parameterJsonNode, ParameterType type) throws CloudFormationException {
Double maxLength = JsonHelper.getDouble(parameterJsonNode, ParameterKey.MaxLength.toString()); // null ok
checkParameterType(maxLength, parameterName, ParameterKey.MaxLength, type, ParameterType.String);
return maxLength;
}
private static String parseConstraintDescription(String parameterName, JsonNode parameterJsonNode) throws CloudFormationException {
String constraintDescription = JsonHelper.getString(parameterJsonNode,
ParameterKey.ConstraintDescription.toString());
// Strangely no length constraints here
return constraintDescription;
}
private static String parseDescription(String parameterName, JsonNode parameterJsonNode) throws CloudFormationException {
String description = JsonHelper.getString(parameterJsonNode,
ParameterKey.Description.toString());
// Strangely no length constraints here (in practice, documentation is wrong currently)
return description;
}
private static String parseAllowedPattern(String parameterName, JsonNode parameterJsonNode, ParameterType type) throws CloudFormationException {
String allowedPattern = JsonHelper.getString(parameterJsonNode, ParameterKey.AllowedPattern.toString()); // null ok
checkParameterType(allowedPattern, parameterName, ParameterKey.AllowedPattern, type, ParameterType.String);
return allowedPattern;
}
private static void checkParameterType(Object value, String name, ParameterKey key, ParameterType valueType, ParameterType requiredType)
throws ValidationErrorException {
if (value != null && valueType != requiredType) {
throw new ValidationErrorException("Template error: Parameter '" + name + "' " + key +
" must be on a parameter of type " + requiredType);
}
}
private static void validateParameterKeys(JsonNode parameterJsonNode) throws ValidationErrorException {
Set<String> tempParameterKeys = Sets.newHashSet(parameterJsonNode.fieldNames());
for (ParameterKey validParameterKey : ParameterKey.values()) {
tempParameterKeys.remove(validParameterKey.toString());
}
if (!tempParameterKeys.isEmpty()) {
throw new ValidationErrorException("Invalid template parameter property or properties " + tempParameterKeys);
}
}
private static String[] parseAllowedValues(JsonNode parameterJsonNode) throws CloudFormationException {
String[] allowedValues = null;
JsonNode allowedValuesJsonNode = JsonHelper.checkArray(parameterJsonNode, ParameterKey.AllowedValues.toString());
if (allowedValuesJsonNode != null) {
allowedValues = new String[allowedValuesJsonNode.size()];
for (int index = 0; index < allowedValues.length; index++) {
String errorMsg = "Every " + ParameterKey.AllowedValues + "value must be a string.";
String allowedValue = JsonHelper.getString(allowedValuesJsonNode, index, errorMsg);
if (allowedValue == null) {
throw new ValidationErrorException("Template format error: " + errorMsg);
}
allowedValues[index] = allowedValue;
}
}
return allowedValues;
}
private static ParameterType parseType(JsonNode parameterJsonNode) throws CloudFormationException {
return parseType(JsonHelper.getString(parameterJsonNode, ParameterKey.Type.toString()));
}
private static ParameterType parseType(String typeStr) throws CloudFormationException {
if (typeStr == null) {
throw new ValidationErrorException("Template format error: Every " + TemplateParser.TemplateSection.Parameters + " object "
+ "must contain a " + ParameterKey.Type + " member.");
}
ParameterType type = null;
try {
type = ParameterType.displayValueOf(typeStr);
} catch (IllegalArgumentException ex) {
throw new ValidationErrorException("Template format error: Unrecognized parameter type: " + typeStr +". Valid values are " + Arrays.toString(ParameterType.displayValues()));
}
return type;
}
private static class Parameter {
private StackEntity.Parameter stackEntityParameter;
private ParameterDeclaration parameterDeclaration;
private Parameter(StackEntity.Parameter stackEntityParameter, ParameterDeclaration parameterDeclaration) {
this.stackEntityParameter = stackEntityParameter;
this.parameterDeclaration = parameterDeclaration;
}
StackEntity.Parameter getParameter() {
return stackEntityParameter;
}
ParameterDeclaration getParameterDeclaration() { return parameterDeclaration; }
}
}
private void parseConditions(Template template, JsonNode templateJsonNode, boolean onlyEvaluateTemplate, String effectiveUserId) throws CloudFormationException {
JsonNode conditionsJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Conditions.toString());
if (conditionsJsonNode == null) return;
Set<String> conditionNames = Sets.newLinkedHashSet(Lists.newArrayList(conditionsJsonNode.fieldNames()));
DependencyManager conditionDependencyManager = new DependencyManager();
for (String conditionName: conditionNames) {
conditionDependencyManager.addNode(conditionName);
}
// Now crawl for dependencies and make sure no resource references...
Set<String> resourceReferences = Sets.newLinkedHashSet();
Set<String> unresolvedConditionDependencies = Sets.newLinkedHashSet();
for (String conditionName: conditionNames) {
JsonNode conditionJsonNode = JsonHelper.checkObject(conditionsJsonNode, conditionName, "Any "
+ TemplateSection.Conditions + " member must be a JSON object.");
conditionDependencyCrawl(conditionName, conditionJsonNode, conditionDependencyManager, template,
resourceReferences, unresolvedConditionDependencies);
FunctionEvaluation.validateConditionSectionArgTypesWherePossible(conditionJsonNode);
}
if (resourceReferences != null && !resourceReferences.isEmpty()) {
throw new ValidationErrorException("Template format error: Unresolved dependencies " +
resourceReferences + ". Cannot reference resources in the Conditions block of the template");
}
if (unresolvedConditionDependencies != null && !resourceReferences.isEmpty()) {
throw new ValidationErrorException("Template format error: Unresolved condition dependencies " +
unresolvedConditionDependencies + " in the Conditions block of the template");
}
try {
for (String conditionName: conditionDependencyManager.dependencyList()) {
JsonNode conditionJsonNode = conditionsJsonNode.get(conditionName);
// Don't like to have to roll/unroll condition map like this but evaluateFunctions is used post-map a lot
Map<String, Boolean> conditionMap = template.getConditionMap();
// just put a placeholder in if evaluating
if (onlyEvaluateTemplate) {
conditionMap.put(conditionName, Boolean.FALSE);
} else {
conditionMap.put(conditionName, FunctionEvaluation.evaluateBoolean(FunctionEvaluation.evaluateFunctions(conditionJsonNode, template, effectiveUserId)));
}
template.setConditionMap(conditionMap);
}
} catch (CyclicDependencyException ex) {
throw new ValidationErrorException("Template error: Found circular condition dependency: " + ex.getMessage());
}
}
private void conditionDependencyCrawl(String originalConditionName, JsonNode currentNode,
DependencyManager conditionDependencyManager, Template template,
Set<String> resourceReferences, Set<String> unresolvedConditionDependencies)
throws CloudFormationException {
if (currentNode == null) return;
if (currentNode.isArray()) {
for (int i = 0;i < currentNode.size(); i++) {
conditionDependencyCrawl(originalConditionName, currentNode.get(i), conditionDependencyManager, template,
resourceReferences, unresolvedConditionDependencies);
}
} else if (!currentNode.isObject()) {
return;
}
// Now we are dealing with an object, perhaps a function
// Check Fn::If
IntrinsicFunction.MatchResult ifMatcher = IntrinsicFunctions.IF.evaluateMatch(currentNode);
if (ifMatcher.isMatch()) {
IntrinsicFunctions.IF.validateArgTypesWherePossible(ifMatcher);
// we have a match against an "if"...
String conditionName = currentNode.get(FunctionEvaluation.FN_IF).get(0).asText();
if (!conditionDependencyManager.containsNode(conditionName)) {
unresolvedConditionDependencies.add(conditionName);
} else {
conditionDependencyManager.addDependency(originalConditionName, conditionName);
}
return;
}
// Check "Condition"
IntrinsicFunction.MatchResult conditionMatcher = IntrinsicFunctions.CONDITION.evaluateMatch(currentNode);
if (conditionMatcher.isMatch()) {
IntrinsicFunctions.CONDITION.validateArgTypesWherePossible(conditionMatcher);
// we have a match against an "condition"...
String conditionName = currentNode.get(FunctionEvaluation.CONDITION_STR).asText();
if (!conditionDependencyManager.containsNode(conditionName)) {
unresolvedConditionDependencies.add(conditionName);
} else {
conditionDependencyManager.addDependency(originalConditionName, conditionName);
}
return;
}
// Check "Ref" (only make sure not resource)
IntrinsicFunction.MatchResult refMatcher = IntrinsicFunctions.REF.evaluateMatch(currentNode);
if (refMatcher.isMatch()) {
IntrinsicFunctions.REF.validateArgTypesWherePossible(refMatcher);
// we have a match against a "ref"...
String refName = currentNode.get(FunctionEvaluation.REF_STR).asText();
// it's ok if it's a psueodparameter or a parameter, but not a resource, or doesn't exist
Map<String, String> pseudoParameterMap = template.getPseudoParameterMap();
Map<String, StackEntity.Parameter> parameterMap = template.getParameterMap();
if (!pseudoParameterMap.containsKey(refName) && !parameterMap.containsKey(refName)) {
resourceReferences.add(refName);
}
return;
}
// Check "Fn::GetAtt" (make sure not resource or attribute)
IntrinsicFunction.MatchResult fnAttMatcher = IntrinsicFunctions.GET_ATT.evaluateMatch(currentNode);
if (fnAttMatcher.isMatch()) {
IntrinsicFunctions.GET_ATT.validateArgTypesWherePossible(fnAttMatcher);
// we have a match against a "ref"...
String refName = currentNode.get(FunctionEvaluation.FN_GET_ATT).get(0).asText();
String attName = currentNode.get(FunctionEvaluation.FN_GET_ATT).get(1).asText();
// Not sure why, but AWS validates attribute types even in Conditions
if (template.getResourceInfoMap().containsKey(refName)) {
ResourceInfo resourceInfo = template.getResourceInfoMap().get(refName);
if (!resourceInfo.isAttributeAllowed(attName)) {
throw new ValidationErrorException("Template error: resource " + refName +
" does not support attribute type " + attName + " in Fn::GetAtt");
} else {
resourceReferences.add(refName);
}
}
return;
}
// Now either just an object or a different function. Either way, crawl in the innards
List<String> fieldNames = Lists.newArrayList(currentNode.fieldNames());
for (String fieldName: fieldNames) {
conditionDependencyCrawl(originalConditionName, currentNode.get(fieldName), conditionDependencyManager, template,
resourceReferences, unresolvedConditionDependencies);
}
}
private void buildResourceMap(Template template, JsonNode templateJsonNode, boolean enforceStrictResourceProperties) throws CloudFormationException {
// This is only done before everything else because Fn::GetAtt needs resource info to determine if it is a
// "good" fit, which is done at "compile time"...
JsonNode resourcesJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Resources.toString());
if (resourcesJsonNode == null || resourcesJsonNode.size() == 0) {
throw new ValidationErrorException("At least one " + TemplateSection.Resources + " member must be defined.");
}
List<String> resourceNames = (List<String>) Lists.newArrayList(resourcesJsonNode.fieldNames());
Map<String, String> pseudoParameterMap = template.getPseudoParameterMap();
String accountId = JsonHelper.getJsonNodeFromString(pseudoParameterMap.get(AWS_ACCOUNT_ID)).asText();
for (String resourceName: resourceNames) {
JsonNode resourceJsonNode = resourcesJsonNode.get(resourceName);
if (!(resourceJsonNode.isObject())) {
throw new ValidationErrorException("Template format error: Any Resources member must be a JSON object.");
}
String type = JsonHelper.getString(resourceJsonNode, ResourceKey.Type.toString());
if (type == null) {
throw new ValidationErrorException("Type is a required property of Resource");
}
ResourceInfo resourceInfo = new ResourceResolverManager().resolveResourceInfo(type);
if (resourceInfo == null) {
throw new ValidationErrorException("Unknown resource type " + type);
}
resourceInfo.setAccountId(accountId);
template.getResourceInfoMap().put(resourceName, resourceInfo);
Set<String> tempResourceKeys = Sets.newHashSet(resourceJsonNode.fieldNames());
for (ResourceKey resourceKey: ResourceKey.values()) {
tempResourceKeys.remove(resourceKey.toString());
}
if (!tempResourceKeys.isEmpty() && enforceStrictResourceProperties) {
throw new ValidationErrorException("Invalid resource property or properties " + tempResourceKeys);
}
}
}
private void parseResources(Template template, JsonNode templateJsonNode, boolean onlyValidateTemplate) throws CloudFormationException {
Map<String, Boolean> conditionMap = template.getConditionMap();
Map<String, String> pseudoParameterMap = template.getPseudoParameterMap();
Map<String, StackEntity.Parameter> parameterMap = template.getParameterMap();
JsonNode resourcesJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Resources.toString());
List<String> resourceKeys = (List<String>) Lists.newArrayList(resourcesJsonNode.fieldNames());
// make sure no duplicates betwen parameters and resources
Set<String> commonParametersAndResources = Sets.intersection(Sets.newHashSet(resourceKeys),
Sets.union(parameterMap.keySet(), pseudoParameterMap.keySet()));
if (!commonParametersAndResources.isEmpty()) {
throw new ValidationErrorException("Template error: all resources and parameters must have unique names. " +
"Common name(s):"+commonParametersAndResources);
}
DependencyManager resourceDependencies = new DependencyManager();
if (resourceKeys.size() > Limits.MAX_RESOURCES_PER_TEMPLATE) {
throw new ValidationErrorException("Too many resources in the template. Max allowed is " + Limits.MAX_RESOURCES_PER_TEMPLATE + ".");
}
for (String resourceKey: resourceKeys) {
if (resourceKey != null && resourceKey.length() > Limits.RESOURCE_NAME_MAX_LENGTH_CHARS) {
throw new ValidationErrorException("Resource name " + resourceKey + " exceeds the maximum resource name length of " + Limits.RESOURCE_NAME_MAX_LENGTH_CHARS + " characters.");
}
if (!resourceKey.matches("^[\\p{Alnum}]*$")) {
throw new ValidationErrorException("Resource name " + resourceKey + " must be alphanumeric.");
}
resourceDependencies.addNode(resourceKey);
}
// evaluate resource dependencies and do some type checking...
Set<String> unresolvedResourceDependencies = Sets.newLinkedHashSet();
for (String resourceKey: resourceKeys) {
JsonNode resourceJsonNode = resourcesJsonNode.get(resourceKey);
JsonNode dependsOnJsonNode = resourceJsonNode.get(ResourceKey.DependsOn.toString());
if (dependsOnJsonNode != null) {
FunctionEvaluation.validateNonConditionSectionArgTypesWherePossible(dependsOnJsonNode);
if (dependsOnJsonNode.isArray()) {
for (int i = 0;i < dependsOnJsonNode.size(); i++) {
if (dependsOnJsonNode.get(i) != null && dependsOnJsonNode.get(i).isValueNode()) {
String dependeningOnResourceName = dependsOnJsonNode.get(i).asText();
if (!template.getResourceInfoMap().containsKey(dependeningOnResourceName)) {
unresolvedResourceDependencies.add(dependeningOnResourceName);
} else {
resourceDependencies.addDependency(resourceKey, dependeningOnResourceName);
}
} else {
throw new ValidationErrorException("Template format error: Every DependsOn value must be a string.");
}
}
} else if (dependsOnJsonNode.isValueNode()) {
String dependeningOnResourceName = dependsOnJsonNode.asText();
if (!template.getResourceInfoMap().containsKey(dependeningOnResourceName)) {
unresolvedResourceDependencies.add(dependeningOnResourceName);
} else {
resourceDependencies.addDependency(resourceKey, dependeningOnResourceName);
}
} else {
throw new ValidationErrorException("Template format error: DependsOn must be a string or list of strings.");
}
}
ResourceInfo resourceInfo = template.getResourceInfoMap().get(resourceKey);
String description = JsonHelper.getString(resourceJsonNode, ResourceKey.Description.toString());
if (description != null && description.length() > 4000) {
throw new ValidationErrorException("Template format error: " + ResourceKey.Description + " must be no "
+ "longer than 4000 characters.");
}
resourceInfo.setDescription(description);
JsonNode metadataNode = JsonHelper.checkObject(resourceJsonNode, ResourceKey.Metadata.toString());
if (metadataNode != null) {
FunctionEvaluation.validateNonConditionSectionArgTypesWherePossible(metadataNode);
resourceInfo.setMetadataJson(JsonHelper.getStringFromJsonNode(metadataNode));
}
JsonNode propertiesNode = JsonHelper.checkObject(resourceJsonNode, ResourceKey.Properties.toString());
if (propertiesNode != null) {
FunctionEvaluation.validateNonConditionSectionArgTypesWherePossible(propertiesNode);
resourceInfo.setPropertiesJson(JsonHelper.getStringFromJsonNode(propertiesNode));
}
JsonNode updatePolicyNode = JsonHelper.checkObject(resourceJsonNode, ResourceKey.UpdatePolicy.toString());
if (propertiesNode != null) {
FunctionEvaluation.validateNonConditionSectionArgTypesWherePossible(propertiesNode);
resourceInfo.setUpdatePolicyJson(JsonHelper.getStringFromJsonNode(updatePolicyNode));
}
JsonNode creationPolicyNode = JsonHelper.checkObject(resourceJsonNode, ResourceKey.CreationPolicy.toString());
if (propertiesNode != null) {
FunctionEvaluation.validateNonConditionSectionArgTypesWherePossible(propertiesNode);
resourceInfo.setCreationPolicyJson(JsonHelper.getStringFromJsonNode(creationPolicyNode));
}
resourceInfo.setLogicalResourceId(resourceKey);
resourceDependencyCrawl(resourceKey, metadataNode, resourceDependencies, template, unresolvedResourceDependencies, !onlyValidateTemplate);
resourceDependencyCrawl(resourceKey, propertiesNode, resourceDependencies, template, unresolvedResourceDependencies, !onlyValidateTemplate);
resourceDependencyCrawl(resourceKey, updatePolicyNode, resourceDependencies, template, unresolvedResourceDependencies, !onlyValidateTemplate);
resourceDependencyCrawl(resourceKey, creationPolicyNode, resourceDependencies, template, unresolvedResourceDependencies, !onlyValidateTemplate);
String deletionPolicy = JsonHelper.getString(resourceJsonNode, ResourceKey.DeletionPolicy.toString());
if (deletionPolicy != null) {
if (!DeletionPolicyValues.Delete.toString().equals(deletionPolicy)
&& !DeletionPolicyValues.Retain.toString().equals(deletionPolicy)
&& !DeletionPolicyValues.Snapshot.toString().equals(deletionPolicy)) {
throw new ValidationErrorException("Template format error: Unrecognized DeletionPolicy " + deletionPolicy +
" for resource " + resourceKey);
}
if (DeletionPolicyValues.Snapshot.toString().equals(deletionPolicy) && !resourceInfo.supportsSnapshot()) {
throw new ValidationErrorException("Template error: resource type " + resourceInfo.getType() + " does not support deletion policy Snapshot");
}
resourceInfo.setDeletionPolicy(deletionPolicy);
}
String conditionKey = JsonHelper.getString(resourceJsonNode, ResourceKey.Condition.toString());
if (conditionKey != null) {
if (!conditionMap.containsKey(conditionKey)) {
throw new ValidationErrorException("Template format error: Condition " + conditionKey + " is not defined.");
}
resourceInfo.setAllowedByCondition((onlyValidateTemplate ? Boolean.TRUE : conditionMap.get(conditionKey)));
} else {
resourceInfo.setAllowedByCondition(Boolean.TRUE);
}
}
if (!unresolvedResourceDependencies.isEmpty()) {
throw new ValidationErrorException("Template format error: Unresolved resource dependencies " + unresolvedResourceDependencies + " in the Resources block of the template");
}
try {
resourceDependencies.dependencyList(); // just to trigger the check...
} catch (CyclicDependencyException ex) {
throw new ValidationErrorException("Circular dependency between resources: " + ex.getMessage());
}
template.setResourceDependencyManager(resourceDependencies);
}
private void resourceDependencyCrawl(String resourceKey, JsonNode jsonNode,
DependencyManager resourceDependencies, Template template,
Set<String> unresolvedResourceDependencies, boolean onLiveBranch)
throws CloudFormationException {
Map<String, String> pseudoParameterMap = template.getPseudoParameterMap();
Map<String, StackEntity.Parameter> parameterMap = template.getParameterMap();
if (jsonNode == null) {
return;
}
if (jsonNode.isArray()) {
for (int i=0;i<jsonNode.size();i++) {
resourceDependencyCrawl(resourceKey, jsonNode.get(i), resourceDependencies, template, unresolvedResourceDependencies, onLiveBranch);
}
}
// Now we are dealing with an object, perhaps a function
// Check "If" (only track dependencies against true branch
IntrinsicFunction.MatchResult fnIfMatcher = IntrinsicFunctions.IF.evaluateMatch(jsonNode);
if (fnIfMatcher.isMatch()) {
IntrinsicFunctions.IF.validateArgTypesWherePossible(fnIfMatcher);
// We know from validate this is an array of 3 elements
JsonNode keyJsonNode = jsonNode.get(FunctionEvaluation.FN_IF);
String key = keyJsonNode.get(0).asText();
Map<String, Boolean> conditionMap = template.getConditionMap();
if (!conditionMap.containsKey(key)) {
throw new ValidationErrorException("Template error: unresolved condition dependency: " + key);
};
boolean booleanValue = template.getConditionMap().get(key);
// AWS has weird behavior that on an Fn::If, undefined Ref values will be detected on branches that are not taken,
// but circular dependencies won't care (as the branch won't be taken)
resourceDependencyCrawl(resourceKey, keyJsonNode.get(1), resourceDependencies, template, unresolvedResourceDependencies, onLiveBranch && booleanValue);
resourceDependencyCrawl(resourceKey, keyJsonNode.get(2), resourceDependencies, template, unresolvedResourceDependencies, onLiveBranch && (!booleanValue));
return;
}
// Check "Ref" (only make sure not resource)
IntrinsicFunction.MatchResult refMatcher = IntrinsicFunctions.REF.evaluateMatch(jsonNode);
if (refMatcher.isMatch()) {
IntrinsicFunctions.REF.validateArgTypesWherePossible(refMatcher);
// we have a match against a "ref"...
String refName = jsonNode.get(FunctionEvaluation.REF_STR).asText();
if (template.getResourceInfoMap().containsKey(refName)) {
if (onLiveBranch) { // the onLiveBranch will add a dependency only if the condition is true
resourceDependencies.addDependency(resourceKey, refName);
}
} else if (!parameterMap.containsKey(refName) &&
!pseudoParameterMap.containsKey(refName)) {
unresolvedResourceDependencies.add(refName);
}
return;
}
// Check "Fn::GetAtt" (make sure not resource or attribute)
IntrinsicFunction.MatchResult fnAttMatcher = IntrinsicFunctions.GET_ATT.evaluateMatch(jsonNode);
if (fnAttMatcher.isMatch()) {
IntrinsicFunctions.GET_ATT.validateArgTypesWherePossible(fnAttMatcher);
// we have a match against a "ref"...
String refName = jsonNode.get(FunctionEvaluation.FN_GET_ATT).get(0).asText();
String attName = jsonNode.get(FunctionEvaluation.FN_GET_ATT).get(1).asText();
// Not sure why, but AWS validates attribute types even in Conditions
if (template.getResourceInfoMap().containsKey(refName)) {
ResourceInfo resourceInfo = template.getResourceInfoMap().get(refName);
if (!resourceInfo.isAttributeAllowed(attName)) {
throw new ValidationErrorException("Template error: resource " + refName +
" does not support attribute type " + attName + " in Fn::GetAtt");
} else {
if (onLiveBranch) { // the onLiveBranch will add a dependency only if the condition is true
resourceDependencies.addDependency(resourceKey, refName);
}
}
} else {
// not a resource...
throw new ValidationErrorException("Template error: instance of Fn::GetAtt references undefined resource "
+ refName);
}
return;
}
// Check "Fn::Sub" ... could be either resource or attribute
IntrinsicFunction.MatchResult fnSubMatcher = IntrinsicFunctions.FN_SUB.evaluateMatch(jsonNode);
if (fnSubMatcher.isMatch()) {
IntrinsicFunctions.FN_SUB.validateArgTypesWherePossible(fnSubMatcher);
// we have a match against a "sub"...
JsonNode subNode = jsonNode.get(FunctionEvaluation.FN_SUB);
Set<String> mappedVariableNames = Sets.newHashSet();
String value;
// either a value node or an array with 2 elements: value and mapping
if (subNode.isValueNode()) {
value = subNode.asText();
} else {
value = subNode.get(0).textValue();
mappedVariableNames.addAll(Lists.newArrayList(subNode.get(1).fieldNames()));
}
Collection<String> variables = FnSubHelper.extractVariables(value);
for (String variable: variables) {
// first see if it is in the accompanying mapping... (or parameter/pseudo-parameter values)
// TODO: fail if wrong type of parameter or pseudoparameter
if (mappedVariableNames.contains(variable) || pseudoParameterMap.containsKey(variable) ||
parameterMap.containsKey(variable)) {
} else if (template.getResourceInfoMap().containsKey(variable)) { // check ref
if (onLiveBranch) { // the onLiveBranch will add a dependency only if the condition is true
resourceDependencies.addDependency(resourceKey, variable);
}
} else if (variable.contains(".")) {
String refName = variable.substring(0, variable.indexOf("."));
String attName = variable.substring(variable.indexOf(".") + 1);
if (template.getResourceInfoMap().containsKey(refName)) {
ResourceInfo resourceInfo = template.getResourceInfoMap().get(refName);
if (!resourceInfo.isAttributeAllowed(attName)) {
throw new ValidationErrorException("Template error: resource " + refName +
" does not support attribute type " + attName + " in Fn::GetAtt");
} else {
if (onLiveBranch) { // the onLiveBranch will add a dependency only if the condition is true
resourceDependencies.addDependency(resourceKey, refName);
}
}
}
} else {
unresolvedResourceDependencies.add(variable);
}
}
return;
}
// Now either just an object or a different function. Either way, crawl in the innards
List<String> fieldNames = Lists.newArrayList(jsonNode.fieldNames());
for (String fieldName: fieldNames) {
resourceDependencyCrawl(resourceKey, jsonNode.get(fieldName), resourceDependencies, template, unresolvedResourceDependencies, onLiveBranch);
}
}
private void parseOutputs(Template template, JsonNode templateJsonNode) throws CloudFormationException {
Map<String, Boolean> conditionMap = template.getConditionMap();
ArrayList<StackEntity.Output> outputs = template.getWorkingOutputs();
JsonNode outputsJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateSection.Outputs.toString());
if (outputsJsonNode != null) {
List<String> unresolvedResourceDependencies = Lists.newArrayList();
List<String> outputKeys = Lists.newArrayList(outputsJsonNode.fieldNames());
for (String outputKey: outputKeys) {
if (outputKey.length() > Limits.OUTPUT_NAME_MAX_LENGTH_CHARS) {
throw new ValidationErrorException("Output " + outputKey + " name exceeds the maximum length of " + Limits.OUTPUT_NAME_MAX_LENGTH_CHARS + " characters");
}
// TODO: we could create an output object, but would have to serialize it to pass to inputs anyway, so just
// parse for now, fail on any errors, and reparse once evaluated.
JsonNode outputJsonNode = outputsJsonNode.get(outputKey);
validateValidResourcesInOutputs(outputKey, outputJsonNode, template, unresolvedResourceDependencies);
Set<String> tempOutputKeys = Sets.newHashSet(outputJsonNode.fieldNames());
for (OutputKey validOutputKey: OutputKey.values()) {
tempOutputKeys.remove(validOutputKey.toString());
}
if (!tempOutputKeys.isEmpty()) {
throw new ValidationErrorException("Invalid output property or properties " + tempOutputKeys);
}
String description = JsonHelper.getString(outputsJsonNode.get(outputKey), OutputKey.Description.toString());
if (description != null && description.length() > 4000) {
throw new ValidationErrorException("Template format error: " + OutputKey.Description + " must be no "
+ "longer than 4000 characters.");
}
String conditionKey = JsonHelper.getString(outputJsonNode, OutputKey.Condition.toString());
if (conditionKey != null) {
if (!conditionMap.containsKey(conditionKey)) {
throw new ValidationErrorException("Template format error: Condition " + conditionKey + " is not defined.");
}
}
if (!outputJsonNode.has(OutputKey.Value.toString())) {
throw new ValidationErrorException("Every Outputs member must contain a Value object");
}
FunctionEvaluation.validateNonConditionSectionArgTypesWherePossible(outputsJsonNode.get(outputKey));
StackEntity.Output output = new StackEntity.Output();
output.setKey(outputKey);
JsonNode outputValueNode = outputJsonNode.get(OutputKey.Value.toString());
boolean match = false;
for (IntrinsicFunction intrinsicFunction: IntrinsicFunctions.values()) {
IntrinsicFunction.MatchResult matchResult = intrinsicFunction.evaluateMatch(outputValueNode);
if (matchResult.isMatch()) {
match = true;
break;
}
}
if (!match) {
if (outputValueNode.isObject()) {
throw new ValidationErrorException("The Value field of every Outputs member must evaluate to a String and not a Map.");
}
if (outputValueNode.isArray()) {
throw new ValidationErrorException("The Value field of every Outputs member must evaluate to a String and not a List.");
}
}
output.setDescription(description);
output.setJsonValue(JsonHelper.getStringFromJsonNode(outputJsonNode.get(OutputKey.Value.toString())));
output.setReady(false);
output.setAllowedByCondition(conditionMap.get(conditionKey) != Boolean.FALSE);
outputs.add(output);
}
if (!unresolvedResourceDependencies.isEmpty()) {
throw new ValidationErrorException("Template format error: Unresolved resource dependencies " + unresolvedResourceDependencies + " in the Outputs block of the template");
}
if (outputs.size() > Limits.MAX_OUTPUTS_PER_TEMPLATE) {
throw new ValidationErrorException("Stack exceeds the maximum allowed number of outputs.("+Limits.MAX_OUTPUTS_PER_TEMPLATE+")");
}
template.setWorkingOutputs(outputs);
}
}
private void validateValidResourcesInOutputs(String outputKey, JsonNode jsonNode, Template template, List<String> unresolvedResourceDependencies) throws CloudFormationException {
Map<String, String> pseudoParameterMap = template.getPseudoParameterMap();
Map<String, StackEntity.Parameter> parameterMap = template.getParameterMap();
Map<String, ResourceInfo> resourceInfoMap = template.getResourceInfoMap();
if (jsonNode == null) {
return;
}
if (jsonNode.isArray()) {
for (int i=0;i<jsonNode.size();i++) {
validateValidResourcesInOutputs(outputKey, jsonNode.get(i), template, unresolvedResourceDependencies);
}
}
// Now we are dealing with an object, perhaps a function
// Check "If" (only track dependencies against true branch
IntrinsicFunction.MatchResult fnIfMatcher = IntrinsicFunctions.IF.evaluateMatch(jsonNode);
// Check "Ref" (only make sure not resource)
IntrinsicFunction.MatchResult refMatcher = IntrinsicFunctions.REF.evaluateMatch(jsonNode);
if (refMatcher.isMatch()) {
IntrinsicFunctions.REF.validateArgTypesWherePossible(refMatcher);
// we have a match against a "ref"...
String refName = jsonNode.get(FunctionEvaluation.REF_STR).asText();
if (!parameterMap.containsKey(refName) &&
!pseudoParameterMap.containsKey(refName) && !resourceInfoMap.containsKey(refName)) {
unresolvedResourceDependencies.add(refName);
}
return;
}
// Check "Fn::GetAtt" (make sure not resource or attribute)
IntrinsicFunction.MatchResult fnAttMatcher = IntrinsicFunctions.GET_ATT.evaluateMatch(jsonNode);
if (fnAttMatcher.isMatch()) {
IntrinsicFunctions.GET_ATT.validateArgTypesWherePossible(fnAttMatcher);
// we have a match against a "ref"...
String refName = jsonNode.get(FunctionEvaluation.FN_GET_ATT).get(0).asText();
String attName = jsonNode.get(FunctionEvaluation.FN_GET_ATT).get(1).asText();
// Not sure why, but AWS validates attribute types even in Conditions
if (resourceInfoMap.containsKey(refName)) {
ResourceInfo resourceInfo = resourceInfoMap.get(refName);
if (!resourceInfo.isAttributeAllowed(attName)) {
throw new ValidationErrorException("Template error: resource " + refName +
" does not support attribute type " + attName + " in Fn::GetAtt");
}
} else {
// not a resource...
throw new ValidationErrorException("Template error: instance of Fn::GetAtt references undefined resource "
+ refName);
}
return;
}
// Now either just an object or a different function. Either way, crawl in the innards
List<String> fieldNames = Lists.newArrayList(jsonNode.fieldNames());
for (String fieldName: fieldNames) {
validateValidResourcesInOutputs(outputKey, jsonNode.get(fieldName), template, unresolvedResourceDependencies);
}
}
public Map<String,ParameterType> getParameterTypeMap(String templateBody) throws CloudFormationException {
Map<String, ParameterType> returnVal = Maps.newHashMap();
JsonNode templateJsonNode;
try {
templateJsonNode = Json.parse( templateBody );
} catch (IOException ex) {
throw new ValidationErrorException(ex.getMessage());
}
if (!templateJsonNode.isObject()) {
throw new ValidationErrorException("Template body is not a JSON object");
}
JsonNode parametersJsonNode = JsonHelper.checkObject(templateJsonNode, TemplateParser.TemplateSection.Parameters.toString());
if (parametersJsonNode != null) {
for (String parameterKey : Lists.newArrayList(parametersJsonNode.fieldNames())) {
JsonNode parameterJsonNode = JsonHelper.checkObject(parametersJsonNode, parameterKey, "Any "
+ TemplateParser.TemplateSection.Parameters + " member must be a JSON object.");
if (parameterJsonNode != null) {
ParameterParser.validateParameterKeys(parameterJsonNode);
returnVal.put(parameterKey, ParameterParser.parseType(parameterJsonNode));
}
}
}
return returnVal;
}
}