/************************************************************************* * Copyright 2013-2014 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.AccessDeniedException; import com.eucalyptus.cloudformation.CloudFormationException; import com.eucalyptus.cloudformation.CloudFormationService; import com.eucalyptus.cloudformation.ValidationErrorException; import com.eucalyptus.cloudformation.entity.StackEntity; import com.eucalyptus.cloudformation.resources.ResourceInfo; import com.eucalyptus.component.ServiceConfiguration; import com.eucalyptus.component.Topology; import com.eucalyptus.compute.common.ClusterInfoType; import com.eucalyptus.compute.common.Compute; import com.eucalyptus.compute.common.DescribeAvailabilityZonesResponseType; import com.eucalyptus.compute.common.DescribeAvailabilityZonesType; import com.eucalyptus.compute.common.DescribeSubnetsResponseType; import com.eucalyptus.compute.common.DescribeSubnetsType; import com.eucalyptus.compute.common.Filter; import com.eucalyptus.compute.common.SubnetType; import com.eucalyptus.util.async.AsyncRequests; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.bouncycastle.util.encoders.Base64; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Pattern; public enum IntrinsicFunctions implements IntrinsicFunction { NO_VALUE { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = ((jsonNode != null) && ( jsonNode.isObject() && jsonNode.size() == 1 && jsonNode.has(FunctionEvaluation.REF_STR) && jsonNode.get(FunctionEvaluation.REF_STR) != null && jsonNode.get(FunctionEvaluation.REF_STR).isValueNode() && FunctionEvaluation.AWS_NO_VALUE.equals(jsonNode.get(FunctionEvaluation.REF_STR).asText()) )); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); return validateResult.getJsonNode(); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { // TODO: not sure in this case, but certainly shouldn't be in Fn::String return false; } }, REF { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.REF_STR)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // This is one where the value literally has to be a string JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.REF_STR); if (keyJsonNode == null || !keyJsonNode.isValueNode()) { throw new ValidationErrorException("Template error: All References must be of type string"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Already known to be string from validate JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.REF_STR); String key = keyJsonNode.asText(); Map<String, String> pseudoParameterMap = template.getPseudoParameterMap(); Map<String, StackEntity.Parameter> parameterMap = template.getParameterMap(); if (pseudoParameterMap.containsKey(key)) { return JsonHelper.getJsonNodeFromString(pseudoParameterMap.get(key)); } else if (parameterMap.containsKey(key)) { return JsonHelper.getJsonNodeFromString(parameterMap.get(key).getJsonValue()); } else if (template.getResourceInfoMap().containsKey(key)) { ResourceInfo resourceInfo = template.getResourceInfoMap().get(key); if (!resourceInfo.getReady()) { throw new ValidationErrorException("Template error: reference " + key + " not ready"); } else { return JsonHelper.getJsonNodeFromString(resourceInfo.getReferenceValueJson()); } } else { throw new ValidationErrorException("Template error: unresolved resource dependency: " + key); } } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, CONDITION { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.CONDITION_STR)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // This is one where the value literally has to be a string JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.CONDITION_STR); if (keyJsonNode == null || !keyJsonNode.isValueNode()) { throw new ValidationErrorException("Template error: All Conditions must be of type string"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Already known to be string from validate JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.CONDITION_STR); String key = keyJsonNode.asText(); Map<String, Boolean> conditionMap = template.getConditionMap(); if (!conditionMap.containsKey(key)) { throw new ValidationErrorException("Template error: unresolved condition dependency: " + key); } return new TextNode(String.valueOf(conditionMap.get(key))); } @Override public boolean isBooleanFunction() { return true; } @Override public boolean mayBeStringFunction() { return false; } }, IF { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_IF)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function returns an array of 3 elements, the first a string that is a condition, and two other functions, // So we check for a literal 3 element array, the first one has to be a string, the other two we can evaluate JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.FN_IF); if (keyJsonNode == null || !keyJsonNode.isArray() || keyJsonNode.size() < 1 || keyJsonNode.get(0) == null || !keyJsonNode.get(0).isValueNode() ) { throw new ValidationErrorException("Template error: Fn::If requires a list argument with the first element " + "being a condition"); } else if (keyJsonNode.size() != 3) { throw new ValidationErrorException("Template error: Fn::If requires a list argument with three elements"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // We know from validate this is an array of 3 elements JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.FN_IF); String key = keyJsonNode.get(0).asText(); boolean booleanValue = template.getConditionMap().get(key); // Note: We are not evaluating both conditions because AWS does not for dependency purposes. Don't want // to get a non-ready reference if we choose the wrong path // But evaluate (as it could be a function) the one we are returning return FunctionEvaluation.evaluateFunctions(keyJsonNode.get(booleanValue ? 1 : 2), template, effectiveUserId); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, EQUALS { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_EQUALS)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // Requires a literal list of two items (so no function evaluation for the list itself) JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.FN_EQUALS); if (keyJsonNode == null || !keyJsonNode.isArray() || keyJsonNode.size() != 2 ) { throw new ValidationErrorException("Template error: Fn::Equals requires a list argument with two elements"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Array verified by validate JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.FN_EQUALS); // On the other hand, the arguments can be functions JsonNode evaluatedArg0 = FunctionEvaluation.evaluateFunctions(keyJsonNode.get(0), template, effectiveUserId); JsonNode evaluatedArg1 = FunctionEvaluation.evaluateFunctions(keyJsonNode.get(1), template, effectiveUserId); // TODO: not sure if this is true if (evaluatedArg0 == null || evaluatedArg1 == null) return new TextNode("false"); return new TextNode(String.valueOf(evaluatedArg0.equals(evaluatedArg1))); } @Override public boolean isBooleanFunction() { return true; } @Override public boolean mayBeStringFunction() { return false; } }, AND { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_AND)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function evaluates to an array of boolean functions so no need to evaluate JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.FN_AND); if (keyJsonNode == null || !keyJsonNode.isArray() || keyJsonNode.size() < 2 || keyJsonNode.size() > 10) { throw new ValidationErrorException("Template error: every Fn::And object requires a list of at least 2 " + "and at most 10 boolean parameters."); } for (int i = 0;i < keyJsonNode.size(); i++) { // Make sure the argument is a function like Fn::Not, Fn::Equals, Fn::And, Fn::Condition JsonNode argNode = keyJsonNode.get(i); if (argNode == null || !argNode.isObject() || !FunctionEvaluation.representsBooleanFunction(argNode)) { throw new ValidationErrorException("Template error: every Fn::And object requires a list of at least 2 " + "and at most 10 boolean parameters."); } } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Args types validated already JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.FN_AND); boolean returnValue = true; for (int i = 0;i < keyJsonNode.size(); i++) { // Evaluate the argument JsonNode argNode = keyJsonNode.get(i); JsonNode evaluatedArgNode = FunctionEvaluation.evaluateFunctions(argNode, template, effectiveUserId); boolean boolValueArgNode = FunctionEvaluation.evaluateBoolean(evaluatedArgNode); returnValue = returnValue && boolValueArgNode; } return new TextNode(String.valueOf(returnValue)); } @Override public boolean isBooleanFunction() { return true; } @Override public boolean mayBeStringFunction() { return false; } }, OR { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_OR)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function evaluates to an array of boolean functions so no need to evaluate JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.FN_OR); if (keyJsonNode == null || !keyJsonNode.isArray() || keyJsonNode.size() < 2 || keyJsonNode.size() > 10) { throw new ValidationErrorException("Template error: every Fn::Or object requires a list of at least 2 " + "and at most 10 boolean parameters."); } for (int i = 0;i < keyJsonNode.size(); i++) { // Make sure the argument is a function like Fn::Not, Fn::Equals, Fn::And, Fn::Condition JsonNode argNode = keyJsonNode.get(i); if (argNode == null || !argNode.isObject() || !FunctionEvaluation.representsBooleanFunction(argNode)) { throw new ValidationErrorException("Template error: every Fn::Or object requires a list of at least 2 " + "and at most 10 boolean parameters."); } } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Args types validated already JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.FN_OR); boolean returnValue = false; for (int i = 0;i < keyJsonNode.size(); i++) { // Evaluate the argument JsonNode argNode = keyJsonNode.get(i); JsonNode evaluatedArgNode = FunctionEvaluation.evaluateFunctions(argNode, template, effectiveUserId); boolean boolValueArgNode = FunctionEvaluation.evaluateBoolean(evaluatedArgNode); returnValue = returnValue || boolValueArgNode; } return new TextNode(String.valueOf(returnValue)); } @Override public boolean isBooleanFunction() { return true; } @Override public boolean mayBeStringFunction() { return false; } }, NOT { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_NOT)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function evaluates to an array of boolean functions so no need to evaluate JsonNode keyJsonNode = matchResult.getJsonNode().get(FunctionEvaluation.FN_NOT); if (keyJsonNode == null || !keyJsonNode.isArray() || keyJsonNode.size() != 1) { throw new ValidationErrorException("Template error: Fn::Not requires a list argument with one element"); } // Make sure the argument is a function like Fn::Not, Fn::Equals, Fn::And, Fn::Condition JsonNode arg0Node = keyJsonNode.get(0); if (arg0Node == null || !arg0Node.isObject() || !FunctionEvaluation.representsBooleanFunction(arg0Node)) { throw new ValidationErrorException("Template error: Fn::Not requires a list argument " + "with one function token"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Args types validated already JsonNode keyJsonNode = validateResult.getJsonNode().get(FunctionEvaluation.FN_NOT); // Evaluate the argument JsonNode arg0Node = keyJsonNode.get(0); JsonNode evaluatedArg0Node = FunctionEvaluation.evaluateFunctions(arg0Node, template, effectiveUserId); boolean boolValueArg0Node = FunctionEvaluation.evaluateBoolean(evaluatedArg0Node); return new TextNode(String.valueOf(!boolValueArg0Node)); } @Override public boolean isBooleanFunction() { return true; } @Override public boolean mayBeStringFunction() { return false; } }, FIND_IN_MAP { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_FIND_IN_MAP)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // Experiments show must be literal array of size 3 (but can have function elements) JsonNode key = matchResult.getJsonNode().get(FunctionEvaluation.FN_FIND_IN_MAP); if (key == null || !key.isArray() || key.size() != 3) { throw new ValidationErrorException("Template error: every Fn::FindInMap object requires three parameters, " + "the map name, map key and the attribute for return value"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Size 3 array verified in validate JsonNode key = validateResult.getJsonNode().get(FunctionEvaluation.FN_FIND_IN_MAP); // Array elements might be functions so evaluate them JsonNode arg0Node = FunctionEvaluation.evaluateFunctions(key.get(0), template, effectiveUserId); JsonNode arg1Node = FunctionEvaluation.evaluateFunctions(key.get(1), template, effectiveUserId); JsonNode arg2Node = FunctionEvaluation.evaluateFunctions(key.get(2), template, effectiveUserId); // Make sure types ok if (arg0Node == null || arg1Node == null || arg2Node == null || !arg0Node.isValueNode() || !arg1Node.isValueNode() || !arg2Node.isValueNode() || arg0Node.asText() == null || arg1Node.asText() == null || arg2Node.asText() == null) { throw new ValidationErrorException("Template error: every Fn::FindInMap object requires three parameters, " + "the map name, map key and the attribute for return value"); } String mapName = arg0Node.asText(); String mapKey = arg1Node.asText(); String attribute = arg2Node.asText(); Map<String, Map<String, Map<String, String>>> mapping = template.getMapping(); if (!mapping.containsKey(mapName)) { throw new ValidationErrorException("Template error: Mapping named '" + mapName + "' is not " + "present in the 'Mappings' section of template"); } if (!mapping.get(mapName).containsKey(mapKey) || !mapping.get(mapName).get(mapKey).containsKey(attribute)) { throw new ValidationErrorException("Template error: Unable to get mapping for " + mapName + "::" + mapKey + "::" + attribute); } return JsonHelper.getJsonNodeFromString(mapping.get(mapName).get(mapKey).get(attribute)); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, BASE64 { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_BASE64)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // no intrinsic evaluation return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // This one could evaluate from a function JsonNode keyJsonNode = FunctionEvaluation.evaluateFunctions(validateResult.getJsonNode().get(FunctionEvaluation.FN_BASE64), template, effectiveUserId); if (keyJsonNode == null || !keyJsonNode.isValueNode()) { throw new ValidationErrorException("Template error: every Fn::Base64 object must have a String-typed value."); } String key = keyJsonNode.asText(); if (key == null) { throw new ValidationErrorException("Template error: every Fn::Base64 object must not have a null value."); } return (key == null) ? validateResult.getJsonNode() : new TextNode(new String(Base64.encode(key.getBytes()))); // TODO: are we just delaying an NPE? } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, SELECT { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_SELECT)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function returns an array with two elements: a string and an array so the top level element is literal JsonNode key = matchResult.getJsonNode().get(FunctionEvaluation.FN_SELECT); if (key == null || !key.isArray() || key.size() < 1) { throw new ValidationErrorException("Template error: Fn::Select requires a list " + "argument with a valid index value as its first element"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Top level array validated already JsonNode key = validateResult.getJsonNode().get(FunctionEvaluation.FN_SELECT); // on the other hand, both fields within this function can be functions (including the second array) so // let's evaluate JsonNode evaluatedIndex = FunctionEvaluation.evaluateFunctions(key.get(0), template, effectiveUserId); if (evaluatedIndex == null || !evaluatedIndex.isValueNode() || evaluatedIndex.asText() == null) { throw new ValidationErrorException("Template error: Fn::Select requires a list " + "argument with a valid index value as its first element"); } int index = -1; try { index = Integer.parseInt(evaluatedIndex.asText()); } catch (NumberFormatException ex) { throw new ValidationErrorException("Template error: Fn::Select requires a list argument with a valid index value as its first element"); } if (key.size() != 2) { throw new ValidationErrorException("Template error: Fn::Select requires a list argument with two elements: an integer index and a list"); } // Second argument must be an array but can be one as the result of a function JsonNode argArray = FunctionEvaluation.evaluateFunctions(key.get(1), template, effectiveUserId); if (argArray == null || !argArray.isArray()) { throw new ValidationErrorException("Template error: Fn::Select requires a list argument with two elements: an integer index and a list"); } if (argArray == null || index < 0 || index >= argArray.size()) { throw new ValidationErrorException("Template error: Fn::Select cannot select nonexistent value at index " + index); } return argArray.get(index); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, JOIN { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_JOIN)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function returns an array with two elements: a string and an array so the top level element is literal JsonNode key = matchResult.getJsonNode().get(FunctionEvaluation.FN_JOIN); if (key == null || !key.isArray() || key.size() != 2) { throw new ValidationErrorException("Template error: every Fn::Join object requires two parameters, " + "(1) a string delimiter and (2) a list of strings to be joined or a function that returns a list of " + "strings (such as Fn::GetAZs) to be joined."); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // Top level array validated already JsonNode key = validateResult.getJsonNode().get(FunctionEvaluation.FN_JOIN); // On the other hand, the delimiter and the list of strings can be functions JsonNode delimiterNode = FunctionEvaluation.evaluateFunctions(key.get(0), template, effectiveUserId); JsonNode arrayOfStrings = FunctionEvaluation.evaluateFunctions(key.get(1), template, effectiveUserId); if (delimiterNode == null || !delimiterNode.isValueNode() || delimiterNode.asText() == null || arrayOfStrings == null || !arrayOfStrings.isArray()) { throw new ValidationErrorException("Template error: every Fn::Join object requires two parameters, " + "(1) a string delimiter and (2) a list of strings to be joined or a function that returns a list of " + "strings (such as Fn::GetAZs) to be joined."); } String delimiter = delimiterNode.asText(); if (arrayOfStrings == null || arrayOfStrings.size() == 0) return new TextNode(""); String tempDelimiter = ""; StringBuilder buffer = new StringBuilder(); for (int i=0;i<arrayOfStrings.size();i++) { if (arrayOfStrings.get(i) == null || !arrayOfStrings.get(i).isValueNode() || arrayOfStrings.get(i).asText() == null) { throw new ValidationErrorException("Template error: every Fn::Join object requires two parameters, (1) " + "a string delimiter and (2) a list of strings to be joined or a function that returns a list of strings" + " (such as Fn::GetAZs) to be joined."); } buffer.append(tempDelimiter).append(arrayOfStrings.get(i).asText()); tempDelimiter = delimiter; } return new TextNode(buffer.toString()); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, GET_AZS { public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_GET_AZS)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // no intrinsic evaluation return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); // This one could evaluate from a function JsonNode keyJsonNode = FunctionEvaluation.evaluateFunctions(validateResult.getJsonNode().get(FunctionEvaluation.FN_GET_AZS), template, effectiveUserId); if (keyJsonNode == null || !keyJsonNode.isValueNode()) { throw new ValidationErrorException("Template error: every Fn::GetAZs object must have a String-typed value."); } String key = keyJsonNode.asText(); if (key == null) { throw new ValidationErrorException("Template error: every Fn::GetAZs object must not have a null value."); } List<String> availabilityZones = Lists.newArrayList(); final List<String> defaultRegionAvailabilityZones; try { defaultRegionAvailabilityZones = describeAvailabilityZones(effectiveUserId); } catch (Exception e) { Throwable rootCause = Throwables.getRootCause(e); throw new AccessDeniedException("Unable to access availability zones. " + (rootCause.getMessage() == null ? "" : rootCause.getMessage())); } final Map<String, List<String>> availabilityZoneMap = Maps.newHashMap(); availabilityZoneMap.put(CloudFormationService.getRegion( ), defaultRegionAvailabilityZones); availabilityZoneMap.put("",defaultRegionAvailabilityZones); // "" defaults to the default region if (availabilityZoneMap != null && availabilityZoneMap.containsKey(key)) { availabilityZones.addAll(availabilityZoneMap.get(key)); } else { // AWS appears to return no values in a different (or non-existant) region so we do the same. } ArrayNode arrayNode = JsonHelper.createArrayNode(); for (String availabilityZone: availabilityZones) { arrayNode.add(availabilityZone); } return arrayNode; } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return false; } private List<String> describeAvailabilityZones(String userId) throws Exception { ServiceConfiguration configuration = Topology.lookup(Compute.class); Map<String, String> defaultSubnetMap = Maps.newHashMap(); DescribeSubnetsType describeSubnetsType = new DescribeSubnetsType(); describeSubnetsType.setEffectiveUserId(userId); Filter defaultSubnetFilter = new Filter(); defaultSubnetFilter.setName("default-for-az"); defaultSubnetFilter.setValueSet(Lists.newArrayList("true")); describeSubnetsType.getFilterSet().add(defaultSubnetFilter); DescribeSubnetsResponseType describeSubnetsResponseType = AsyncRequests.sendSync( configuration, describeSubnetsType ); if (describeSubnetsResponseType != null && describeSubnetsResponseType.getSubnetSet() != null && describeSubnetsResponseType.getSubnetSet().getItem() != null) { for (SubnetType subnetType: describeSubnetsResponseType.getSubnetSet().getItem()) { if (subnetType.getVpcId() != null) { defaultSubnetMap.put(subnetType.getAvailabilityZone(), subnetType.getSubnetId()); } } } boolean atLeastOneAZHasDefaultSubnet = false; List<String> availabilityZonesWithDefaultSubnet = Lists.newArrayList(); DescribeAvailabilityZonesType describeAvailabilityZonesType = new DescribeAvailabilityZonesType(); describeAvailabilityZonesType.setEffectiveUserId(userId); DescribeAvailabilityZonesResponseType describeAvailabilityZonesResponseType = AsyncRequests.<DescribeAvailabilityZonesType,DescribeAvailabilityZonesResponseType> sendSync(configuration, describeAvailabilityZonesType); List<String> allAvailabilityZones = Lists.newArrayList(); for (ClusterInfoType clusterInfoType: describeAvailabilityZonesResponseType.getAvailabilityZoneInfo()) { allAvailabilityZones.add(clusterInfoType.getZoneName()); if (defaultSubnetMap.containsKey(clusterInfoType.getZoneName())) { atLeastOneAZHasDefaultSubnet = true; availabilityZonesWithDefaultSubnet.add(clusterInfoType.getZoneName()); } } return atLeastOneAZHasDefaultSubnet ? availabilityZonesWithDefaultSubnet : allAvailabilityZones; } }, FN_SUB { public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_SUB)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); JsonNode key = matchResult.getJsonNode().get(FunctionEvaluation.FN_SUB); if (key == null || !( (key.isValueNode()) || (key.isArray() && key.size() == 2 && key.get(0) != null && key.get(0).isValueNode() && key.get(1) != null && key.get(1).isObject())) ) { throw new ValidationErrorException("Template error: One or more Fn::Sub intrinsic functions don't " + "specify expected arguments. Specify a string as first argument, and an optional second " + "argument to specify a mapping of values to replace in the string"); } // a little checking of field values if (key.isValueNode()) { FnSubHelper.extractVariables(key.asText()); } else { FnSubHelper.extractVariables(key.get(0).asText()); checkValidSubstitutionKeys(key.get(1).fieldNames()); checkValidStringOrStringFunctions(key.get(1)); } return new ValidateResult(matchResult.getJsonNode(), this); } private void checkValidSubstitutionKeys(Iterator<String> stringIterator) throws ValidationErrorException { if (stringIterator != null) { for (String key: (Iterable<String>) ()-> stringIterator) { Pattern pattern = Pattern.compile("[A-Za-z0-9_]+"); if (!pattern.matcher(key).matches()) { throw new ValidationErrorException("Template error: every key of the context object of every Fn::Sub object " + "must contain only alphanumeric characters and underscores"); } } } } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); Map<String, String> pseudoParameterMap = template.getPseudoParameterMap(); Map<String, StackEntity.Parameter> parameterMap = template.getParameterMap(); JsonNode key = validateResult.getJsonNode().get(FunctionEvaluation.FN_SUB); String value; // a little checking of field values Map<String, String> variableMapping = Maps.newHashMap(); if (key.isValueNode()) { value = key.asText(); } else { value = key.get(0).asText(); for (String fieldName : Lists.newArrayList(key.get(1).fieldNames())) { JsonNode valueNode = FunctionEvaluation.evaluateFunctions(key.get(1).get(fieldName), template, effectiveUserId); if (valueNode == null || !valueNode.isValueNode()) { throw new ValidationErrorException("Template error: every value of the context object of every Fn::Sub object must be a string or a function that returns a string"); } variableMapping.put(fieldName, valueNode.asText()); } } Collection<String> variables = FnSubHelper.extractVariables(value); // check variables for (String variable: variables) { if (!variableMapping.containsKey(variable)) { if (pseudoParameterMap.containsKey(variable)) { checkAndAddJsonNodeToVariableMapping(variable, variableMapping, pseudoParameterMap.get(variable)); } else if (parameterMap.containsKey(variable)) { checkAndAddJsonNodeToVariableMapping(variable, variableMapping, parameterMap.get(variable).getJsonValue()); } else if (template.getResourceInfoMap().containsKey(variable)) { ResourceInfo resourceInfo = template.getResourceInfoMap().get(variable); if (!resourceInfo.getReady()) { throw new ValidationErrorException("Template error: reference " + key + " not ready"); } else { checkAndAddJsonNodeToVariableMapping(variable, variableMapping, resourceInfo.getReferenceValueJson()); } } else if (variable.indexOf(".") == -1) { throw new ValidationErrorException("Unresolved resource dependencies [" + variable + "] in the Resources block of the template"); } else { int dotPos = variable.indexOf("."); String resourceName = variable.substring(0, dotPos); String attributeName = variable.substring(dotPos + 1); if (template.getResourceInfoMap().containsKey(resourceName) && template.getResourceInfoMap().get(resourceName).isAttributeAllowed(attributeName)) { ResourceInfo resourceInfo = template.getResourceInfoMap().get(resourceName); if (!resourceInfo.getReady()) { throw new ValidationErrorException("Template error: reference " + resourceName + " not ready"); } try { checkAndAddJsonNodeToVariableMapping(variable, variableMapping, resourceInfo.getResourceAttributeJson(attributeName)); } catch (Exception ex) { throw new ValidationErrorException("Template error: resource " + resourceName + " does not support " + "attribute type " + attributeName + " in Fn::GetAtt"); } } else { throw new ValidationErrorException("Template error: instance of Fn::Sub references invalid resource attribute " + variable); } } } } return new TextNode(FnSubHelper.replaceVariables(value, variableMapping)); } private void checkValidStringOrStringFunctions(JsonNode jsonNode) throws ValidationErrorException { for (String fieldName: Lists.newArrayList(jsonNode.fieldNames())) { JsonNode valueNode = jsonNode.get(fieldName); if (valueNode == null || !(valueNode.isValueNode() || FunctionEvaluation.mayRepresentStringFunction(valueNode))) { throw new ValidationErrorException("Template error: every value of the context object of every Fn::Sub object must be a string or a function that returns a string"); } } } private void checkAndAddJsonNodeToVariableMapping(String variable, Map<String, String> variableMapping, String jsonString) throws ValidationErrorException { JsonNode jsonNode = JsonHelper.getJsonNodeFromString(jsonString); if (jsonNode == null || !jsonNode.isValueNode()) { throw new ValidationErrorException("Template error: every variable in an Fn::sub object must reference a string"); } variableMapping.put(variable, jsonNode.asText()); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, GET_ATT { public MatchResult evaluateMatch(JsonNode jsonNode) { boolean match = (jsonNode != null && jsonNode.isObject() && (jsonNode.size() == 1) && jsonNode.has(FunctionEvaluation.FN_GET_ATT)); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // No function returns an array with two elements: a string and an array so the top level element is literal JsonNode key = matchResult.getJsonNode().get(FunctionEvaluation.FN_GET_ATT); if (key == null || !key.isArray() || key.size() != 2 || key.get(0) == null || !key.get(0).isValueNode() || key.get(0).asText() == null || key.get(0).asText().isEmpty() || key.get(1) == null || !key.get(1).isValueNode() || key.get(1).asText() == null || key.get(1).asText().isEmpty()) { throw new ValidationErrorException("Template error: every Fn::GetAtt object requires two non-empty parameters, " + "the resource name and the resource attribute"); } return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); JsonNode key = validateResult.getJsonNode().get(FunctionEvaluation.FN_GET_ATT); String resourceName = key.get(0).asText(); if (!template.getResourceInfoMap().containsKey(resourceName)) { throw new ValidationErrorException("Template error: instance of Fn::GetAtt references undefined resource " + resourceName); } ResourceInfo resourceInfo = template.getResourceInfoMap().get(resourceName); if (!resourceInfo.getReady()) { throw new ValidationErrorException("Template error: reference " + resourceName + " not ready"); } String attributeName = key.get(1).asText(); try { return JsonHelper.getJsonNodeFromString(resourceInfo.getResourceAttributeJson(attributeName)); } catch (Exception ex) { throw new ValidationErrorException("Template error: resource " + resourceName + " does not support " + "attribute type " + attributeName + " in Fn::GetAtt"); } } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return true; } }, UNKNOWN { @Override public MatchResult evaluateMatch(JsonNode jsonNode) { // Something that starts with Fn: (any existing functions will already have been evaluated) boolean match = ((jsonNode != null) && ( jsonNode.isObject() && jsonNode.size() == 1 && jsonNode.fieldNames().next().startsWith("Fn:") )); return new MatchResult(match, jsonNode, this); } @Override public ValidateResult validateArgTypesWherePossible(MatchResult matchResult) throws CloudFormationException { checkState(matchResult, this); // no intrinsic evaluation return new ValidateResult(matchResult.getJsonNode(), this); } @Override public JsonNode evaluateFunction(ValidateResult validateResult, Template template, String effectiveUserId) throws CloudFormationException { checkState(validateResult, this); throw new ValidationErrorException("Template Error: Encountered unsupported function: " + validateResult.getJsonNode().fieldNames().next()+" Supported functions are: [Fn::Base64, Fn::GetAtt, " + "Fn::GetAZs, Fn::Join, Fn::FindInMap, Fn::Select, Ref, Fn::Equals, Fn::If, Fn::Not, " + "Condition, Fn::And, Fn::Or, Fn::Sub]"); } @Override public boolean isBooleanFunction() { return false; } @Override public boolean mayBeStringFunction() { return false; } }; public abstract boolean isBooleanFunction(); public abstract boolean mayBeStringFunction(); protected void checkState(MatchResult matchResult, IntrinsicFunction intrinsicFunction) { if (matchResult == null || matchResult.isMatch() == false || !intrinsicFunction.equals(matchResult.getCallingFunction())) { throw new IllegalStateException("MatchResult passed in is null, false or used with the wrong function"); } } protected void checkState(ValidateResult validateResult, IntrinsicFunction intrinsicFunction) { if (validateResult == null || !equals(validateResult.getCallingFunction())) { throw new IllegalStateException("ValidateResult passed in is null, false or used with the wrong function"); } } }