/************************************************************************* * Copyright 2009-2015 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; import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflow; import com.amazonaws.services.simpleworkflow.model.DescribeWorkflowExecutionRequest; import com.amazonaws.services.simpleworkflow.model.RequestCancelWorkflowExecutionRequest; import com.amazonaws.services.simpleworkflow.model.WorkflowExecution; import com.amazonaws.services.simpleworkflow.model.WorkflowExecutionDetail; import com.eucalyptus.auth.Accounts; import com.eucalyptus.auth.AuthQuotaException; import com.eucalyptus.auth.Permissions; import com.eucalyptus.auth.euare.identity.region.RegionConfigurations; import com.eucalyptus.auth.principal.User; import com.eucalyptus.auth.tokens.SecurityTokenAWSCredentialsProvider; import com.eucalyptus.cloudformation.common.policy.CloudFormationPolicySpec; import com.eucalyptus.cloudformation.config.CloudFormationProperties; import com.eucalyptus.cloudformation.entity.DeleteStackWorkflowExtraInfoEntity; import com.eucalyptus.cloudformation.entity.DeleteStackWorkflowExtraInfoEntityManager; import com.eucalyptus.cloudformation.entity.SignalEntity; import com.eucalyptus.cloudformation.entity.SignalEntityManager; import com.eucalyptus.cloudformation.entity.StackEntity; import com.eucalyptus.cloudformation.entity.StackEntityHelper; import com.eucalyptus.cloudformation.entity.StackEntityManager; import com.eucalyptus.cloudformation.entity.StackEventEntityManager; import com.eucalyptus.cloudformation.entity.StackResourceEntity; import com.eucalyptus.cloudformation.entity.StackResourceEntityManager; import com.eucalyptus.cloudformation.entity.StackUpdateInfoEntityManager; import com.eucalyptus.cloudformation.entity.StackWorkflowEntity; import com.eucalyptus.cloudformation.entity.StackWorkflowEntityManager; import com.eucalyptus.cloudformation.entity.Status; import com.eucalyptus.cloudformation.entity.VersionedStackEntity; import com.eucalyptus.cloudformation.resources.ResourceInfo; import com.eucalyptus.cloudformation.template.FunctionEvaluation; import com.eucalyptus.cloudformation.template.JsonHelper; import com.eucalyptus.cloudformation.template.PseudoParameterValues; import com.eucalyptus.cloudformation.template.Template; import com.eucalyptus.cloudformation.template.TemplateParser; import com.eucalyptus.cloudformation.template.url.S3Helper; import com.eucalyptus.cloudformation.template.url.WhiteListURLMatcher; import com.eucalyptus.cloudformation.util.CfnIdentityDocumentCredential; import com.eucalyptus.cloudformation.workflow.CommonDeleteRollbackKickoff; import com.eucalyptus.cloudformation.workflow.CreateStackWorkflow; import com.eucalyptus.cloudformation.workflow.CreateStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.CreateStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.MonitorCreateStackWorkflow; import com.eucalyptus.cloudformation.workflow.MonitorCreateStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.MonitorCreateStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.MonitorUpdateStackWorkflow; import com.eucalyptus.cloudformation.workflow.MonitorUpdateStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.MonitorUpdateStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.StartTimeoutPassableWorkflowClientFactory; import com.eucalyptus.cloudformation.workflow.UpdateStackPartsWorkflowKickOff; import com.eucalyptus.cloudformation.workflow.UpdateStackWorkflow; import com.eucalyptus.cloudformation.workflow.UpdateStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.UpdateStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.WorkflowClientManager; import com.eucalyptus.cloudformation.ws.StackWorkflowTags; import com.eucalyptus.component.ComponentIds; import com.eucalyptus.component.annotation.ComponentNamed; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.crypto.util.SslSetup; import com.eucalyptus.objectstorage.ObjectStorage; import com.eucalyptus.objectstorage.client.EucaS3Client; import com.eucalyptus.objectstorage.client.EucaS3ClientFactory; import com.eucalyptus.objectstorage.util.ObjectStorageProperties; import com.eucalyptus.util.Exceptions; import com.eucalyptus.util.IO; import com.eucalyptus.util.Json; import com.eucalyptus.util.RestrictedTypes; import com.eucalyptus.util.dns.DomainNames; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.HashMultiset; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multiset; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import com.netflix.glisten.InterfaceBasedWorkflowClient; import com.netflix.glisten.WorkflowDescriptionTemplate; import com.netflix.glisten.WorkflowTags; import org.apache.commons.io.input.BoundedInputStream; import org.apache.log4j.Logger; import org.xbill.DNS.Name; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; @ConfigurableClass( root = "cloudformation", description = "Parameters controlling cloud formation") @ComponentNamed public class CloudFormationService { public static final String NO_UPDATES_ARE_TO_BE_PERFORMED = "No updates are to be performed."; @ConfigurableField(initial = "", description = "The value of AWS::Region and value in CloudFormation ARNs for Region") public static volatile String REGION = ""; @ConfigurableField(initial = "*.s3.amazonaws.com", description = "A comma separated white list of domains (other than Eucalyptus S3 URLs) allowed by CloudFormation URL parameters") public static volatile String URL_DOMAIN_WHITELIST = "*s3.amazonaws.com"; private static final String NO_ECHO_PARAMETER_VALUE = "****"; private static final String STACK_ID_PREFIX = "arn:aws:cloudformation:"; private static final Logger LOG = Logger.getLogger(CloudFormationService.class); public CancelUpdateStackResponseType cancelUpdateStack( CancelUpdateStackType request ) throws CloudFormationException { CancelUpdateStackResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = ctx.getAccountNumber(); final String accountAlias = ctx.getAccountAlias(); final String stackName = request.getStackName(); if (stackName == null) throw new ValidationErrorException("Stack name is null"); StackEntity stackEntity = StackEntityManager.getNonDeletedStackByNameOrId(stackName, accountId); if ( stackEntity == null && ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ) { stackEntity = StackEntityManager.getNonDeletedStackByNameOrId(stackName, null); } if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } if ( !RestrictedTypes.filterPrivileged( ).apply( stackEntity ) ) { throw new AccessDeniedException( "Not authorized." ); } if (stackEntity.getStackStatus() != Status.UPDATE_IN_PROGRESS) { throw new ValidationErrorException("CancelUpdateStack cannot be called from current stack status."); } final String stackAccountId = stackEntity.getAccountId( ); // check to see if there is an update workflow. : boolean existingOpenUpdateWorkflow = false; List<StackWorkflowEntity> updateWorkflows = StackWorkflowEntityManager.getStackWorkflowEntities(stackEntity.getStackId(), StackWorkflowEntity.WorkflowType.UPDATE_STACK_WORKFLOW); if ( updateWorkflows != null && !updateWorkflows.isEmpty( ) ) { if (updateWorkflows.size() > 1) { throw new ValidationErrorException("More than one update workflow exists for " + stackEntity.getStackId()); // TODO: InternalFailureException (?) } try { AmazonSimpleWorkflow simpleWorkflowClient = WorkflowClientManager.getSimpleWorkflowClient(); StackWorkflowEntity updateStackWorkflowEntity = updateWorkflows.get(0); DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest = new DescribeWorkflowExecutionRequest(); describeWorkflowExecutionRequest.setDomain(updateStackWorkflowEntity.getDomain()); WorkflowExecution execution = new WorkflowExecution(); execution.setRunId(updateStackWorkflowEntity.getRunId()); execution.setWorkflowId(updateStackWorkflowEntity.getWorkflowId()); describeWorkflowExecutionRequest.setExecution(execution); WorkflowExecutionDetail workflowExecutionDetail = simpleWorkflowClient.describeWorkflowExecution(describeWorkflowExecutionRequest); if ("OPEN".equals(workflowExecutionDetail.getExecutionInfo().getExecutionStatus())) { RequestCancelWorkflowExecutionRequest requestCancelWorkflowExecutionRequest = new RequestCancelWorkflowExecutionRequest(); requestCancelWorkflowExecutionRequest.setDomain(updateStackWorkflowEntity.getDomain()); requestCancelWorkflowExecutionRequest.setRunId(updateStackWorkflowEntity.getRunId()); requestCancelWorkflowExecutionRequest.setWorkflowId(updateStackWorkflowEntity.getWorkflowId()); simpleWorkflowClient.requestCancelWorkflowExecution(requestCancelWorkflowExecutionRequest); } } catch (Exception ex) { LOG.error(ex); LOG.debug(ex, ex); throw new ValidationErrorException("Unable to cancel update workflow for " + stackEntity.getStackId()); } } } catch (Exception ex) { handleException(ex); } return reply; } public ContinueUpdateRollbackResponseType continueUpdateRollbackStackResponseType (final ContinueUpdateRollbackType request ) throws CloudFormationException { ContinueUpdateRollbackResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = ctx.getAccountNumber(); final String accountAlias = ctx.getAccountAlias(); final String stackName = request.getStackName(); if (stackName == null) throw new ValidationErrorException("Stack name is null"); StackEntity stackEntity = StackEntityManager.getNonDeletedStackByNameOrId(stackName, accountId); if ( stackEntity == null && ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ) { stackEntity = StackEntityManager.getNonDeletedStackByNameOrId(stackName, null); } if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } if ( !RestrictedTypes.filterPrivileged( ).apply( stackEntity ) ) { throw new AccessDeniedException( "Not authorized." ); } String outerStackArn = StackResourceEntityManager.findOuterStackArnIfExists(stackEntity.getStackId(), accountId); if (outerStackArn != null) { throw new ValidationErrorException("Failed to rollback: RollbackUpdatedStack cannot be invoked on child stacks"); } final String stackAccountId = stackEntity.getAccountId( ); if (stackEntity.getStackStatus() != Status.UPDATE_ROLLBACK_FAILED) { throw new ValidationErrorException("Stack " + stackEntity.getStackId() + " is in " + stackEntity.getStackStatus() + " state and can not continue to update rollback."); } // check to see if there has been a continue update rollback stack workflow. If one exists and is still going on, just quit: boolean existingOpenContinueUpdateRollbackWorkflow = false; List<StackWorkflowEntity> continueUpdateRollbackWorkflows = StackWorkflowEntityManager.getStackWorkflowEntities(stackEntity.getStackId(), StackWorkflowEntity.WorkflowType.UPDATE_ROLLBACK_STACK_WORKFLOW); if ( continueUpdateRollbackWorkflows != null && !continueUpdateRollbackWorkflows.isEmpty( ) ) { if (continueUpdateRollbackWorkflows.size() > 1) { throw new ValidationErrorException("More than one continue update rollback workflow exists for " + stackEntity.getStackId()); // TODO: InternalFailureException (?) } // see if the workflow is open try { AmazonSimpleWorkflow simpleWorkflowClient = WorkflowClientManager.getSimpleWorkflowClient(); StackWorkflowEntity continueUpdateRollbackWorkflowEntity = continueUpdateRollbackWorkflows.get(0); DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest = new DescribeWorkflowExecutionRequest(); describeWorkflowExecutionRequest.setDomain(continueUpdateRollbackWorkflowEntity.getDomain()); WorkflowExecution execution = new WorkflowExecution(); execution.setRunId(continueUpdateRollbackWorkflowEntity.getRunId()); execution.setWorkflowId(continueUpdateRollbackWorkflowEntity.getWorkflowId()); describeWorkflowExecutionRequest.setExecution(execution); WorkflowExecutionDetail workflowExecutionDetail = simpleWorkflowClient.describeWorkflowExecution(describeWorkflowExecutionRequest); if ("OPEN".equals(workflowExecutionDetail.getExecutionInfo().getExecutionStatus())) { existingOpenContinueUpdateRollbackWorkflow = true; } } catch (Exception ex) { LOG.error("Unable to get status of continue update rollback workflow for " + stackEntity.getStackId() + ", assuming not open"); LOG.debug(ex); } } if (!existingOpenContinueUpdateRollbackWorkflow) { UpdateStackPartsWorkflowKickOff.kickOffUpdateRollbackStackWorkflow(stackEntity.getStackId(), stackEntity.getAccountId(), outerStackArn, userId); } } catch (Exception ex) { handleException(ex); } return reply; } public CreateStackResponseType createStack( final CreateStackType request ) throws CloudFormationException { CreateStackResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = ctx.getAccountNumber(); final String accountAlias = ctx.getAccountAlias(); final String stackName = request.getStackName(); final String templateBody = request.getTemplateBody(); final String templateUrl = request.getTemplateURL(); final String stackPolicyBody = request.getStackPolicyBody(); final String stackPolicyUrl = request.getStackPolicyURL(); final String stackPolicyText = validateAndGetStackPolicy(user, stackPolicyBody, stackPolicyUrl); if (stackName == null) throw new ValidationErrorException("Stack name is null"); if (!stackName.matches("^[\\p{Alpha}][\\p{Alnum}-]*$")) { throw new ValidationErrorException("Stack name " + stackName + " must contain only letters, numbers, dashes and start with an alpha character."); } if (stackName.length() > Limits.STACK_NAME_MAX_LENGTH_CHARS) { throw new ValidationErrorException("Stack name " + stackName + " must be no longer than " + Limits.STACK_NAME_MAX_LENGTH_CHARS + " characters."); } if (templateBody == null && templateUrl == null) throw new ValidationErrorException("Either TemplateBody or TemplateURL must be set."); if (templateBody != null && templateUrl != null) throw new ValidationErrorException("Exactly one of TemplateBody or TemplateURL must be set."); List<Parameter> parameters = null; if (request.getParameters() != null && request.getParameters().getMember() != null) { parameters = request.getParameters().getMember(); } final String stackIdLocal = UUID.randomUUID().toString(); final String stackId = STACK_ID_PREFIX + REGION + ":" + accountId + ":stack/"+stackName+"/"+stackIdLocal; final PseudoParameterValues pseudoParameterValues = new PseudoParameterValues(); pseudoParameterValues.setAccountId(accountId); pseudoParameterValues.setStackName(stackName); pseudoParameterValues.setStackId(stackId); if (request.getNotificationARNs() != null && request.getNotificationARNs().getMember() != null) { ArrayList<String> notificationArns = Lists.newArrayList(); for (String notificationArn: request.getNotificationARNs().getMember()) { notificationArns.add(notificationArn); } pseudoParameterValues.setNotificationArns(notificationArns); } pseudoParameterValues.setRegion(getRegion()); final ArrayList<String> capabilities = Lists.newArrayList(); if (request.getCapabilities() != null && request.getCapabilities().getMember() != null) { for (String capability: request.getCapabilities().getMember()) { TemplateParser.Capabilities capabilityEnum = null; try { capabilityEnum = TemplateParser.Capabilities.valueOf(capability); } catch (Exception ex) { } if (capabilityEnum == null) { throw new ValidationErrorException("Capability " + capability + " is not a valid capability. Valid values are " + Lists.newArrayList(TemplateParser.Capabilities.values())); } capabilities.add(capability); } } if (templateBody != null) { if (templateBody.getBytes().length > Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES) { throw new ValidationErrorException("Template body may not exceed " + Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES + " bytes in a request."); } } if (request.getTags()!= null && request.getTags().getMember() != null) { for (Tag tag: request.getTags().getMember()) { if (Strings.isNullOrEmpty(tag.getKey()) || Strings.isNullOrEmpty(tag.getValue())) { throw new ValidationErrorException("Tags can not be null or empty"); } else if (tag.getKey().startsWith("aws:") ) { throw new ValidationErrorException("Invalid tag key. \"aws:\" is a reserved prefix."); } else if (tag.getKey().startsWith("euca:") ) { throw new ValidationErrorException("Invalid tag key. \"euca:\" is a reserved prefix."); } } } final String templateText = (templateBody != null) ? templateBody : extractTemplateTextFromURL(templateUrl, user); final Template template = new TemplateParser().parse(templateText, parameters, capabilities, pseudoParameterValues, userId, CloudFormationProperties.ENFORCE_STRICT_RESOURCE_PROPERTIES); final Supplier<StackEntity> allocator = new Supplier<StackEntity>() { @Override public StackEntity get() { try { StackEntity stackEntity = new StackEntity(); final int INIT_STACK_VERSION = 0; StackEntityHelper.populateStackEntityWithTemplate(stackEntity, template); stackEntity.setStackName(stackName); stackEntity.setStackId(stackId); stackEntity.setNaturalId(stackIdLocal); stackEntity.setAccountId(accountId); stackEntity.setTemplateBody(templateText); stackEntity.setStackPolicy(stackPolicyText); stackEntity.setStackStatus(Status.CREATE_IN_PROGRESS); stackEntity.setStackStatusReason("User initiated"); stackEntity.setDisableRollback(Boolean.TRUE.equals(request.getDisableRollback())); // null -> false stackEntity.setCreationTimestamp(new Date()); if (request.getCapabilities() != null && request.getCapabilities().getMember() != null) { stackEntity.setCapabilitiesJson(StackEntityHelper.capabilitiesToJson(capabilities)); } if (request.getNotificationARNs()!= null && request.getNotificationARNs().getMember() != null) { stackEntity.setNotificationARNsJson(StackEntityHelper.notificationARNsToJson(request.getNotificationARNs().getMember())); } if (request.getTags()!= null && request.getTags().getMember() != null) { stackEntity.setTagsJson(StackEntityHelper.tagsToJson(request.getTags().getMember())); } stackEntity.setStackVersion(INIT_STACK_VERSION); stackEntity.setRecordDeleted(Boolean.FALSE); stackEntity = (StackEntity) StackEntityManager.addStack(stackEntity); // TODO: Arguably everything after here should be considered not part of the allocation of the stack entity String onFailure; if (request.getOnFailure() != null && !request.getOnFailure().isEmpty()) { if (!request.getOnFailure().equals("ROLLBACK") && !request.getOnFailure().equals("DELETE") && !request.getOnFailure().equals("DO_NOTHING")) { throw new ValidationErrorException("Value '" + request.getOnFailure() + "' at 'onFailure' failed to satisfy " + "constraint: Member must satisfy enum value set: [ROLLBACK, DELETE, DO_NOTHING]"); } else { onFailure = request.getOnFailure(); } } else { onFailure = (Boolean.TRUE.equals(request.getDisableRollback())) ? "DO_NOTHING" : "ROLLBACK"; } for (ResourceInfo resourceInfo: template.getResourceInfoMap().values()) { StackResourceEntity stackResourceEntity = new StackResourceEntity(); stackResourceEntity = StackResourceEntityManager.updateResourceInfo(stackResourceEntity, resourceInfo); stackResourceEntity.setDescription(""); // TODO: maybe on resource info? stackResourceEntity.setResourceStatus(Status.NOT_STARTED); stackResourceEntity.setStackId(stackId); stackResourceEntity.setStackName(stackName); stackResourceEntity.setResourceVersion(INIT_STACK_VERSION); stackResourceEntity.setRecordDeleted(Boolean.FALSE); StackResourceEntityManager.addStackResource(stackResourceEntity); } StackWorkflowTags stackWorkflowTags = new StackWorkflowTags(stackId, stackName, accountId, accountAlias); Long timeoutInSeconds = (request.getTimeoutInMinutes() != null && request.getTimeoutInMinutes()> 0 ? 60L * request.getTimeoutInMinutes() : null); StartTimeoutPassableWorkflowClientFactory createStackWorkflowClientFactory = new StartTimeoutPassableWorkflowClientFactory(WorkflowClientManager.getSimpleWorkflowClient( ), CloudFormationProperties.SWF_DOMAIN, CloudFormationProperties.SWF_TASKLIST); WorkflowDescriptionTemplate createStackWorkflowDescriptionTemplate = new CreateStackWorkflowDescriptionTemplate(); InterfaceBasedWorkflowClient<CreateStackWorkflow> createStackWorkflowClient = createStackWorkflowClientFactory .getNewWorkflowClient(CreateStackWorkflow.class, createStackWorkflowDescriptionTemplate, stackWorkflowTags, timeoutInSeconds, null); CreateStackWorkflow createStackWorkflow = new CreateStackWorkflowClient(createStackWorkflowClient); createStackWorkflow.createStack(stackEntity.getStackId(), stackEntity.getAccountId(), stackEntity.getResourceDependencyManagerJson(), userId, onFailure, INIT_STACK_VERSION); StackWorkflowEntityManager.addOrUpdateStackWorkflowEntity(stackId, StackWorkflowEntity.WorkflowType.CREATE_STACK_WORKFLOW, CloudFormationProperties.SWF_DOMAIN, createStackWorkflowClient.getWorkflowExecution().getWorkflowId(), createStackWorkflowClient.getWorkflowExecution().getRunId()); StartTimeoutPassableWorkflowClientFactory monitorCreateStackWorkflowClientFactory = new StartTimeoutPassableWorkflowClientFactory(WorkflowClientManager.getSimpleWorkflowClient(), CloudFormationProperties.SWF_DOMAIN, CloudFormationProperties.SWF_TASKLIST); WorkflowDescriptionTemplate monitorCreateStackWorkflowDescriptionTemplate = new MonitorCreateStackWorkflowDescriptionTemplate(); InterfaceBasedWorkflowClient<MonitorCreateStackWorkflow> monitorCreateStackWorkflowClient = monitorCreateStackWorkflowClientFactory .getNewWorkflowClient(MonitorCreateStackWorkflow.class, monitorCreateStackWorkflowDescriptionTemplate, stackWorkflowTags, null, null); MonitorCreateStackWorkflow monitorCreateStackWorkflow = new MonitorCreateStackWorkflowClient(monitorCreateStackWorkflowClient); monitorCreateStackWorkflow.monitorCreateStack(stackId, stackName, accountId, accountAlias, stackEntity.getResourceDependencyManagerJson(), userId, onFailure, INIT_STACK_VERSION); StackWorkflowEntityManager.addOrUpdateStackWorkflowEntity(stackId, StackWorkflowEntity.WorkflowType.MONITOR_CREATE_STACK_WORKFLOW, CloudFormationProperties.SWF_DOMAIN, monitorCreateStackWorkflowClient.getWorkflowExecution().getWorkflowId(), monitorCreateStackWorkflowClient.getWorkflowExecution().getRunId()); return stackEntity; } catch ( CloudFormationException e ) { throw Exceptions.toUndeclared( e ); } } }; try { final StackEntity stackEntity = RestrictedTypes.allocateUnitlessResource(allocator); } catch (AuthQuotaException e) { throw new LimitExceededException(e.getMessage()); } CreateStackResult createStackResult = new CreateStackResult(); createStackResult.setStackId(stackId); reply.setCreateStackResult(createStackResult); } catch (Exception ex) { handleException(ex); } return reply; } private static String extractStackPolicyDuringUpdateFromURL(String stackPolicyUrl, User user) throws ValidationErrorException { return extractTextFromURL("Stack Policy During Update URL", URL_DOMAIN_WHITELIST, Limits.REQUEST_STACK_POLICY_MAX_CONTENT_LENGTH_BYTES, stackPolicyUrl, user); } private static String extractStackPolicyFromURL(String stackPolicyUrl, User user) throws ValidationErrorException { return extractTextFromURL("Stack Policy URL", URL_DOMAIN_WHITELIST, Limits.REQUEST_STACK_POLICY_MAX_CONTENT_LENGTH_BYTES, stackPolicyUrl, user); } private static String extractTemplateTextFromURL(String templateUrl, User user) throws ValidationErrorException { return extractTextFromURL("Template URL", URL_DOMAIN_WHITELIST, Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES, templateUrl, user); } private static String extractTextFromURL(String urlType, String whitelist, long maxContentLength, String urlStr, User user) throws ValidationErrorException { final URL url; try { url = new URL(urlStr); } catch (MalformedURLException e) { throw new ValidationErrorException("Invalid " + urlType + ":" + urlStr); } // First try straight HTTP GET if url is in whitelist boolean inWhitelist = WhiteListURLMatcher.urlIsAllowed(url, whitelist); if (inWhitelist) { InputStream templateIn = null; try { final URLConnection connection = SslSetup.configureHttpsUrlConnection( url.openConnection( ) ); templateIn = connection.getInputStream( ); long contentLength = connection.getContentLengthLong( ); if ( contentLength > maxContentLength) { throw new ValidationErrorException(urlType + " exceeds maximum byte count, " + maxContentLength); } final byte[] templateData = ByteStreams.toByteArray( new BoundedInputStream( templateIn, maxContentLength + 1 ) ); if ( templateData.length > maxContentLength) { throw new ValidationErrorException(urlType + " exceeds maximum byte count, " + maxContentLength); } return new String( templateData, StandardCharsets.UTF_8 ); } catch ( UnknownHostException ex ) { throw new ValidationErrorException("Invalid " + urlType + ":" + urlStr); } catch ( SSLHandshakeException ex ) { throw new ValidationErrorException("HTTPS connection error for " + urlStr ); } catch (IOException ex) { if ( Strings.nullToEmpty( ex.getMessage( ) ).startsWith( "HTTPS hostname wrong" ) ) { throw new ValidationErrorException( "HTTPS connection failed hostname verification for " + urlStr ); } LOG.info("Unable to connect to whitelisted URL, trying S3 instead"); LOG.debug(ex, ex); } finally { IO.close( templateIn ); } } // Otherwise, assume the URL is a eucalyptus S3 url... String[] validHostBucketSuffixes = new String[]{"walrus", "objectstorage", "s3"}; String[] validServicePaths = new String[]{ObjectStorageProperties.LEGACY_WALRUS_SERVICE_PATH, ComponentIds.lookup(ObjectStorage.class).getServicePath()}; String[] validDomains = new String[]{DomainNames.externalSubdomain().relativize( Name.root ).toString( )}; S3Helper.BucketAndKey bucketAndKey = S3Helper.getBucketAndKeyFromUrl(url, validServicePaths, validHostBucketSuffixes, validDomains); try ( final EucaS3Client eucaS3Client = EucaS3ClientFactory.getEucaS3Client( SecurityTokenAWSCredentialsProvider.forUserOrRole( user ) ) ) { if (eucaS3Client.getObjectMetadata(bucketAndKey.getBucket(), bucketAndKey.getKey()).getContentLength() > maxContentLength) { throw new ValidationErrorException(urlType + " exceeds maximum byte count, " + maxContentLength); } return eucaS3Client.getObjectContent( bucketAndKey.getBucket( ), bucketAndKey.getKey( ), (int) maxContentLength ); } catch (Exception ex) { LOG.debug("Error getting s3 object content: " + bucketAndKey.getBucket() + "/" + bucketAndKey.getKey()); LOG.debug(ex, ex); throw new ValidationErrorException(urlType + " is an S3 URL to a non-existent or unauthorized bucket/key. (bucket=" + bucketAndKey.getBucket() + ", key=" + bucketAndKey.getKey()); } } private EnumSet<Status> inProgressCantDeleteStatuses = EnumSet.of( Status.UPDATE_IN_PROGRESS, Status.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS, Status.UPDATE_ROLLBACK_IN_PROGRESS, Status.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS ); private EnumSet<StackWorkflowEntity.WorkflowType> updateOrMonitorUpdateWorkflowTypes = EnumSet.of( StackWorkflowEntity.WorkflowType.UPDATE_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.UPDATE_CLEANUP_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.UPDATE_ROLLBACK_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.UPDATE_ROLLBACK_CLEANUP_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.MONITOR_UPDATE_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.MONITOR_UPDATE_CLEANUP_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.MONITOR_UPDATE_ROLLBACK_STACK_WORKFLOW, StackWorkflowEntity.WorkflowType.MONITOR_UPDATE_ROLLBACK_CLEANUP_STACK_WORKFLOW ); public DeleteStackResponseType deleteStack( final DeleteStackType request ) throws CloudFormationException { DeleteStackResponseType reply = request.getReply(); String retainedResourcesStr = ""; if (request.getRetainResources() != null && request.getRetainResources().getMember() != null && !request.getRetainResources().getMember().isEmpty()) { retainedResourcesStr = Joiner.on(",").join(request.getRetainResources().getMember()); } try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = ctx.getAccountNumber(); final String accountAlias = ctx.getAccountAlias(); final String stackName = request.getStackName(); if (stackName == null) throw new ValidationErrorException("Stack name is null"); StackEntity stackEntity = StackEntityManager.getNonDeletedStackByNameOrId(stackName, accountId); if ( stackEntity == null && ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ) { stackEntity = StackEntityManager.getNonDeletedStackByNameOrId(stackName, null); } if ( stackEntity != null ) { if ( !RestrictedTypes.filterPrivileged( ).apply( stackEntity ) ) { throw new AccessDeniedException( "Not authorized." ); } final String stackAccountId = stackEntity.getAccountId( ); final String stackAccountAlias = Accounts.lookupAccountAliasById(stackAccountId); // eucalyptus administrators act as account admin to delete resources final String userId = ctx.isAdministrator( ) ? Accounts.lookupCachedPrincipalByAccountNumber( stackAccountId ).getUserId( ) : user.getUserId( ); String stackId = stackEntity.getStackId(); if (inProgressCantDeleteStatuses.contains(stackEntity.getStackStatus()) && hasOpenWorkflowOfType(stackEntity, updateOrMonitorUpdateWorkflowTypes)) { throw new ValidationErrorException("Stack " + stackEntity.getStackId() + " is in " + stackEntity.getStackStatus() + " state and can not be deleted."); } // check to see if there has been a delete workflow. If one exists and is still going on, just quit: // unless its retain resources list doesn't match what is passed in. boolean existingOpenDeleteWorkflow = false; boolean existingOpenDeleteWorkflowWithDifferentRetainedResources = false; List<StackWorkflowEntity> deleteWorkflows = StackWorkflowEntityManager.getStackWorkflowEntities(stackEntity.getStackId(), StackWorkflowEntity.WorkflowType.DELETE_STACK_WORKFLOW); if ( deleteWorkflows != null && !deleteWorkflows.isEmpty( ) ) { if (deleteWorkflows.size() > 1) { throw new ValidationErrorException("More than one delete workflow exists for " + stackEntity.getStackId()); // TODO: InternalFailureException (?) } // see if the workflow is open try { AmazonSimpleWorkflow simpleWorkflowClient = WorkflowClientManager.getSimpleWorkflowClient(); StackWorkflowEntity deleteStackWorkflowEntity = deleteWorkflows.get(0); DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest = new DescribeWorkflowExecutionRequest(); describeWorkflowExecutionRequest.setDomain(deleteStackWorkflowEntity.getDomain()); WorkflowExecution execution = new WorkflowExecution(); execution.setRunId(deleteStackWorkflowEntity.getRunId()); execution.setWorkflowId(deleteStackWorkflowEntity.getWorkflowId()); describeWorkflowExecutionRequest.setExecution(execution); WorkflowExecutionDetail workflowExecutionDetail = simpleWorkflowClient.describeWorkflowExecution(describeWorkflowExecutionRequest); if ("OPEN".equals(workflowExecutionDetail.getExecutionInfo().getExecutionStatus())) { existingOpenDeleteWorkflow = true; String currentWorkflowRetainedResources = getRetainedResourcesFromCurrentOpenWorkflow(stackEntity, deleteStackWorkflowEntity); if (!Strings.isNullOrEmpty(retainedResourcesStr) && !Objects.equals(retainedResourcesStr, currentWorkflowRetainedResources)) { existingOpenDeleteWorkflowWithDifferentRetainedResources = true; } } } catch (Exception ex) { LOG.error("Unable to get status of delete workflow for " + stackEntity.getStackId() + ", assuming not open"); LOG.debug(ex); } } if (existingOpenDeleteWorkflowWithDifferentRetainedResources) { throw new ValidationErrorException("A delete stack operation is already in progress for stack " + stackEntity.getStackId()+ ". " + "Do not submit another delete stack request specifying different resources to retain or resources to retain in a different order."); } if (!existingOpenDeleteWorkflow) { if (!Strings.isNullOrEmpty(retainedResourcesStr) && stackEntity.getStackStatus() != Status.DELETE_FAILED) { throw new ValidationErrorException("Invalid operation on stack " + stackEntity.getStackId() + ". When " + "you delete a stack, specify which resources to retain only when the stack is in the DELETE_FAILED state."); } if (!Strings.isNullOrEmpty(retainedResourcesStr)) { Set<String> alreadyDeletedResources = Sets.newHashSet(); Set<String> realResources = Sets.newHashSet(); // see that we don't try to delete resources that are already deleted for (StackResourceEntity stackResourceEntity : StackResourceEntityManager.describeStackResources(accountId, stackEntity.getStackId())) { if (stackResourceEntity.getResourceStatus() == Status.DELETE_COMPLETE) { alreadyDeletedResources.add(stackResourceEntity.getLogicalResourceId()); } realResources.add(stackResourceEntity.getLogicalResourceId()); } for (String retainedResource : Splitter.on(",").omitEmptyStrings().split(retainedResourcesStr)) { if (alreadyDeletedResources.contains(retainedResource) || !realResources.contains(retainedResource)) { throw new ValidationErrorException("The specified resources to retain must be in a valid state. Do not " + "specify resources that are in the DELETE_COMPLETE state."); } } } String resourceDependencyManagerJson = stackEntity.getResourceDependencyManagerJson(); int stackVersion = stackEntity.getStackVersion(); CommonDeleteRollbackKickoff.kickOffDeleteStackWorkflow(userId, stackId, stackName, stackAccountId, stackAccountAlias, resourceDependencyManagerJson, stackVersion, retainedResourcesStr); } } } catch (Exception ex) { handleException(ex); } return reply; } private boolean hasOpenWorkflowOfType(StackEntity stackEntity, Collection<StackWorkflowEntity.WorkflowType> workflowTypes) throws ValidationErrorException { for (StackWorkflowEntity.WorkflowType workflowType: workflowTypes) { List<StackWorkflowEntity> workflows = StackWorkflowEntityManager.getStackWorkflowEntities(stackEntity.getStackId(), workflowType); if ( workflows != null && !workflows.isEmpty( ) ) { if (workflows.size() > 1) { throw new ValidationErrorException("More than one " + workflowType + "workflow exists for " + stackEntity.getStackId()); // TODO: InternalFailureException (?) } // see if the workflow is open try { AmazonSimpleWorkflow simpleWorkflowClient = WorkflowClientManager.getSimpleWorkflowClient(); StackWorkflowEntity deleteStackWorkflowEntity = workflows.get(0); DescribeWorkflowExecutionRequest describeWorkflowExecutionRequest = new DescribeWorkflowExecutionRequest(); describeWorkflowExecutionRequest.setDomain(deleteStackWorkflowEntity.getDomain()); WorkflowExecution execution = new WorkflowExecution(); execution.setRunId(deleteStackWorkflowEntity.getRunId()); execution.setWorkflowId(deleteStackWorkflowEntity.getWorkflowId()); describeWorkflowExecutionRequest.setExecution(execution); WorkflowExecutionDetail workflowExecutionDetail = simpleWorkflowClient.describeWorkflowExecution(describeWorkflowExecutionRequest); if ("OPEN".equals(workflowExecutionDetail.getExecutionInfo().getExecutionStatus())) { return true; } } catch (Exception ex) { LOG.error("Unable to get status of " + workflowType + " workflow for " + stackEntity.getStackId() + ", assuming not open"); LOG.debug(ex); } } } return false; } private String getRetainedResourcesFromCurrentOpenWorkflow(StackEntity stackEntity, StackWorkflowEntity deleteStackWorkflowEntity) { String currentWorkflowRetainedResources = ""; List<DeleteStackWorkflowExtraInfoEntity> extraInfoEntityList = DeleteStackWorkflowExtraInfoEntityManager.getExtraInfoEntities(stackEntity.getStackId()); if (extraInfoEntityList != null) { for (DeleteStackWorkflowExtraInfoEntity extraInfoEntity: extraInfoEntityList) { if (deleteStackWorkflowEntity.getDomain().equals(extraInfoEntity.getDomain()) && deleteStackWorkflowEntity.getRunId().equals(extraInfoEntity.getRunId()) && deleteStackWorkflowEntity.getWorkflowId().equals(extraInfoEntity.getWorkflowId())) { currentWorkflowRetainedResources = extraInfoEntity.getRetainedResourcesStr(); break; } } } return currentWorkflowRetainedResources; } public DescribeStackEventsResponseType describeStackEvents( final DescribeStackEventsType request ) throws CloudFormationException { DescribeStackEventsResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); User user = ctx.getUser(); String accountId = user.getAccountNumber(); String stackName = request.getStackName(); if (stackName == null) throw new ValidationErrorException("Stack name is null"); checkStackPermission( ctx, stackName, accountId ); ArrayList<StackEvent> stackEventList = StackEventEntityManager.getStackEventsByNameOrId( stackName, accountId ); if ( stackEventList.isEmpty( ) && ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ) { stackEventList = StackEventEntityManager.getStackEventsByNameOrId( stackName, null ); } StackEvents stackEvents = new StackEvents(); stackEvents.setMember(stackEventList); DescribeStackEventsResult describeStackEventsResult = new DescribeStackEventsResult(); describeStackEventsResult.setStackEvents(stackEvents); reply.setDescribeStackEventsResult(describeStackEventsResult); } catch (Exception ex) { handleException(ex); } return reply; } public DescribeStackResourceResponseType describeStackResource(DescribeStackResourceType request) throws CloudFormationException { DescribeStackResourceResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final String stackName = request.getStackName(); if (stackName == null) throw new ValidationErrorException("Stack name is null"); checkStackPermission( ctx, stackName, accountId, true ); final String logicalResourceId = request.getLogicalResourceId(); if (logicalResourceId == null) throw new ValidationErrorException("logicalResourceId is null"); final StackResourceEntity stackResourceEntity = StackResourceEntityManager.describeStackResource( ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ? null : accountId, stackName, logicalResourceId ); final StackResourceDetail stackResourceDetail = new StackResourceDetail(); stackResourceDetail.setDescription(stackResourceEntity.getDescription()); stackResourceDetail.setLastUpdatedTimestamp(stackResourceEntity.getLastUpdateTimestamp()); stackResourceDetail.setLogicalResourceId(stackResourceEntity.getLogicalResourceId()); stackResourceDetail.setMetadata(stackResourceEntity.getMetadataJson()); stackResourceDetail.setPhysicalResourceId(stackResourceEntity.getPhysicalResourceId()); stackResourceDetail.setResourceStatus(stackResourceEntity.getResourceStatus() == null ? null : stackResourceEntity.getResourceStatus().toString()); stackResourceDetail.setResourceStatusReason(stackResourceEntity.getResourceStatusReason()); stackResourceDetail.setResourceType(stackResourceEntity.getResourceType()); stackResourceDetail.setStackId(stackResourceEntity.getStackId()); stackResourceDetail.setStackName(stackResourceEntity.getStackName()); final DescribeStackResourceResult describeStackResourceResult = new DescribeStackResourceResult(); describeStackResourceResult.setStackResourceDetail(stackResourceDetail); reply.setDescribeStackResourceResult(describeStackResourceResult); } catch (Exception ex) { handleException(ex); } return reply; } public DescribeStackResourcesResponseType describeStackResources(final DescribeStackResourcesType request) throws CloudFormationException { DescribeStackResourcesResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final String stackName = request.getStackName(); final String logicalResourceId = request.getLogicalResourceId(); final String physicalResourceId = request.getPhysicalResourceId(); if ( Strings.isNullOrEmpty( stackName ) && Strings.isNullOrEmpty( physicalResourceId ) ) { throw new ValidationErrorException("StackName or PhysicalResourceId required"); } final ArrayList<StackResource> stackResourceList = Lists.newArrayList(); final List<StackResourceEntity> stackResourceEntityList = StackResourceEntityManager.describeStackResources( ctx.isAdministrator( ) && stackName!=null && stackName.startsWith( STACK_ID_PREFIX ) ? null : accountId, stackName, physicalResourceId, logicalResourceId ); if (stackResourceEntityList != null && !stackResourceEntityList.isEmpty()) { checkStackPermission( ctx, stackResourceEntityList.get( 0 ).getStackId( ), accountId ); for (StackResourceEntity stackResourceEntity: stackResourceEntityList) { StackResource stackResource = new StackResource(); stackResource.setDescription(stackResourceEntity.getDescription()); stackResource.setLogicalResourceId(stackResourceEntity.getLogicalResourceId()); stackResource.setPhysicalResourceId(stackResourceEntity.getPhysicalResourceId()); stackResource.setResourceStatus(stackResourceEntity.getResourceStatus().toString()); stackResource.setResourceStatusReason(stackResourceEntity.getResourceStatusReason()); stackResource.setResourceType(stackResourceEntity.getResourceType()); stackResource.setStackId(stackResourceEntity.getStackId()); stackResource.setStackName(stackResourceEntity.getStackName()); stackResource.setTimestamp(stackResourceEntity.getLastUpdateTimestamp()); stackResourceList.add(stackResource); } } final DescribeStackResourcesResult describeStackResourcesResult = new DescribeStackResourcesResult(); final StackResources stackResources = new StackResources(); stackResources.setMember(stackResourceList); describeStackResourcesResult.setStackResources(stackResources); reply.setDescribeStackResourcesResult(describeStackResourcesResult); } catch (Exception ex) { handleException(ex); } return reply; } public DescribeStacksResponseType describeStacks(DescribeStacksType request) throws CloudFormationException { DescribeStacksResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final String stackName = request.getStackName(); final List<StackEntity> stackEntities = StackEntityManager.describeStacks( ctx.isAdministrator( ) && stackName!=null && ("verbose".equals(stackName) || stackName.startsWith( STACK_ID_PREFIX )) ? null : accountId, ctx.isAdministrator( ) && "verbose".equals(stackName) ? null : stackName ); final ArrayList<Stack> stackList = new ArrayList<Stack>(); for ( final StackEntity stackEntity : Iterables.filter( stackEntities, RestrictedTypes.filterPrivileged( ) ) ) { Stack stack = new Stack(); if (stackEntity.getCapabilitiesJson() != null && !stackEntity.getCapabilitiesJson().isEmpty()) { ResourceList capabilities = new ResourceList(); ArrayList<String> member = StackEntityHelper.jsonToCapabilities(stackEntity.getCapabilitiesJson()); capabilities.setMember(member); stack.setCapabilities(capabilities); } stack.setCreationTime(stackEntity.getCreateOperationTimestamp()); stack.setDescription(stackEntity.getDescription()); stack.setStackName(stackEntity.getStackName()); stack.setDisableRollback(stackEntity.getDisableRollback()); // TODO: how do we handle onFailure(?) field stack.setLastUpdatedTime(stackEntity.getLastUpdateTimestamp()); if (stackEntity.getNotificationARNsJson() != null && !stackEntity.getNotificationARNsJson().isEmpty()) { ResourceList notificationARNs = new ResourceList(); ArrayList<String> member = StackEntityHelper.jsonToNotificationARNs(stackEntity.getNotificationARNsJson()); notificationARNs.setMember(member); stack.setNotificationARNs(notificationARNs); } if (stackEntity.getOutputsJson() != null && !stackEntity.getOutputsJson().isEmpty()) { boolean somethingNotReady = false; ArrayList<StackEntity.Output> stackEntityOutputs = StackEntityHelper.jsonToOutputs(stackEntity.getOutputsJson()); ArrayList<Output> member = Lists.newArrayList(); for (StackEntity.Output stackEntityOutput: stackEntityOutputs) { if (!stackEntityOutput.isReady()) { somethingNotReady = true; break; } else if (stackEntityOutput.isAllowedByCondition()) { Output output = new Output(); output.setDescription(stackEntityOutput.getDescription()); output.setOutputKey(stackEntityOutput.getKey()); output.setOutputValue(stackEntityOutput.getStringValue()); member.add(output); } } if (!somethingNotReady) { Outputs outputs = new Outputs(); outputs.setMember(member); stack.setOutputs(outputs); } } if (stackEntity.getParametersJson() != null && !stackEntity.getParametersJson().isEmpty()) { ArrayList<StackEntity.Parameter> stackEntityParameters = StackEntityHelper.jsonToParameters(stackEntity.getParametersJson()); ArrayList<Parameter> member = Lists.newArrayList(); for (StackEntity.Parameter stackEntityParameter: stackEntityParameters) { Parameter parameter = new Parameter(); parameter.setParameterKey(stackEntityParameter.getKey()); parameter.setParameterValue(stackEntityParameter.isNoEcho() ? NO_ECHO_PARAMETER_VALUE : stackEntityParameter.getStringValue()); member.add(parameter); } Parameters parameters = new Parameters(); parameters.setMember(member); stack.setParameters(parameters); } stack.setStackId(stackEntity.getStackId()); stack.setStackName(stackEntity.getStackName()); stack.setStackStatus(stackEntity.getStackStatus().toString()); stack.setStackStatusReason(stackEntity.getStackStatusReason()); if (stackEntity.getTagsJson() != null && !stackEntity.getTagsJson().isEmpty()) { Tags tags = new Tags(); ArrayList<Tag> member = StackEntityHelper.jsonToTags(stackEntity.getTagsJson()); tags.setMember(member); stack.setTags(tags); } stack.setTimeoutInMinutes(stackEntity.getTimeoutInMinutes()); stackList.add(stack); } DescribeStacksResult describeStacksResult = new DescribeStacksResult(); Stacks stacks = new Stacks(); stacks.setMember(stackList ); describeStacksResult.setStacks(stacks ); reply.setDescribeStacksResult(describeStacksResult); } catch (Exception ex) { handleException(ex); } return reply; } public EstimateTemplateCostResponseType estimateTemplateCost(EstimateTemplateCostType request) throws CloudFormationException { return request.getReply(); } public GetStackPolicyResponseType getStackPolicy(final GetStackPolicyType request) throws CloudFormationException { GetStackPolicyResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final String stackName = request.getStackName(); if (stackName == null) { throw new ValidationErrorException("StackName must not be null"); } checkStackPermission( ctx, stackName, accountId ); final StackEntity stackEntity = StackEntityManager.getAnyStackByNameOrId( stackName, ctx.isAdministrator() && stackName.startsWith(STACK_ID_PREFIX) ? null : accountId); if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } GetStackPolicyResult getStackPolicyResult = new GetStackPolicyResult(); getStackPolicyResult.setStackPolicyBody(stackEntity.getStackPolicy()); reply.setGetStackPolicyResult(getStackPolicyResult); } catch (Exception ex) { handleException(ex); } return reply; } public GetTemplateResponseType getTemplate(final GetTemplateType request) throws CloudFormationException { GetTemplateResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final String stackName = request.getStackName(); if (stackName == null) { throw new ValidationErrorException("StackName must not be null"); } checkStackPermission( ctx, stackName, accountId ); final StackEntity stackEntity = StackEntityManager.getAnyStackByNameOrId( stackName, ctx.isAdministrator() && stackName.startsWith(STACK_ID_PREFIX) ? null : accountId); if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } GetTemplateResult getTemplateResult = new GetTemplateResult(); getTemplateResult.setTemplateBody(stackEntity.getTemplateBody()); reply.setGetTemplateResult(getTemplateResult); } catch (Exception ex) { handleException(ex); } return reply; } public GetTemplateSummaryResponseType getTemplateSummary(GetTemplateSummaryType request) throws CloudFormationException { GetTemplateSummaryResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = user.getAccountNumber(); final String templateBody = request.getTemplateBody(); final String templateUrl = request.getTemplateURL(); final String stackName = request.getStackName(); int numNonNullParamsInTemplateBodyTemplateURLAndStackName = 0; if (templateBody != null) numNonNullParamsInTemplateBodyTemplateURLAndStackName++; if (templateUrl != null) numNonNullParamsInTemplateBodyTemplateURLAndStackName++; if (stackName != null) numNonNullParamsInTemplateBodyTemplateURLAndStackName++; if (numNonNullParamsInTemplateBodyTemplateURLAndStackName == 0) throw new ValidationErrorException("Either StackName or TemplateBody or TemplateURL must be set."); if (numNonNullParamsInTemplateBodyTemplateURLAndStackName > 1) throw new ValidationErrorException("Exactly one of StackName or TemplateBody or TemplateURL must be set."); String templateText; // IAM Action Check if (stackName != null) { checkStackPermission( ctx, stackName, accountId ); final StackEntity stackEntity = StackEntityManager.getAnyStackByNameOrId( stackName, ctx.isAdministrator() && stackName.startsWith(STACK_ID_PREFIX) ? null : accountId); if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } templateText = stackEntity.getTemplateBody(); } else { checkActionPermission(CloudFormationPolicySpec.CLOUDFORMATION_GETTEMPLATESUMMARY, ctx); if (templateBody != null) { if (templateBody.getBytes().length > Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES) { throw new ValidationErrorException("Template body may not exceed " + Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES + " bytes in a request."); } } templateText = (templateBody != null) ? templateBody : extractTemplateTextFromURL(templateUrl, user); } final String stackIdLocal = UUID.randomUUID().toString(); final String stackId = "arn:aws:cloudformation:" + REGION + ":" + accountId + ":stack/"+stackName+"/"+stackIdLocal; final PseudoParameterValues pseudoParameterValues = new PseudoParameterValues(); pseudoParameterValues.setAccountId(accountId); pseudoParameterValues.setStackName(stackName); pseudoParameterValues.setStackId(stackId); ArrayList<String> notificationArns = Lists.newArrayList(); pseudoParameterValues.setRegion(getRegion()); List<Parameter> parameters = Lists.newArrayList(); final GetTemplateSummaryResult getTemplateSummaryResult = new TemplateParser().getTemplateSummary(templateText, parameters, pseudoParameterValues, userId, CloudFormationProperties.ENFORCE_STRICT_RESOURCE_PROPERTIES); reply.setGetTemplateSummaryResult(getTemplateSummaryResult); } catch (Exception ex) { handleException(ex); } return reply; } public ListStackResourcesResponseType listStackResources(ListStackResourcesType request) throws CloudFormationException { ListStackResourcesResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final String stackName = request.getStackName(); if (stackName == null) { throw new ValidationErrorException("StackName must not be null"); } checkStackPermission( ctx, stackName, accountId ); ArrayList<StackResourceSummary> stackResourceSummaryList = Lists.newArrayList(); List<StackResourceEntity> stackResourceEntityList = StackResourceEntityManager.listStackResources( ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ? null : accountId, stackName ); if (stackResourceEntityList != null) { for (StackResourceEntity stackResourceEntity: stackResourceEntityList) { StackResourceSummary stackResourceSummary = new StackResourceSummary(); stackResourceSummary.setLogicalResourceId(stackResourceEntity.getLogicalResourceId()); stackResourceSummary.setPhysicalResourceId(stackResourceEntity.getPhysicalResourceId()); stackResourceSummary.setResourceStatus(stackResourceEntity.getResourceStatus().toString()); stackResourceSummary.setResourceStatusReason(stackResourceEntity.getResourceStatusReason()); stackResourceSummary.setResourceType(stackResourceEntity.getResourceType()); stackResourceSummary.setLastUpdatedTimestamp(stackResourceEntity.getLastUpdateTimestamp()); stackResourceSummaryList.add(stackResourceSummary); } } ListStackResourcesResult listStackResourcesResult = new ListStackResourcesResult(); StackResourceSummaries stackResourceSummaries = new StackResourceSummaries(); stackResourceSummaries.setMember(stackResourceSummaryList); listStackResourcesResult.setStackResourceSummaries(stackResourceSummaries); reply.setListStackResourcesResult(listStackResourcesResult); } catch (Exception ex) { handleException(ex); } return reply; } public ListStacksResponseType listStacks(ListStacksType request) throws CloudFormationException { ListStacksResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); final ResourceList stackStatusFilter = request.getStackStatusFilter(); final List<Status> statusFilterList = Lists.newArrayList(); if (stackStatusFilter != null && stackStatusFilter.getMember() != null) { for (String statusFilterStr: stackStatusFilter.getMember()) { try { statusFilterList.add(Status.valueOf(statusFilterStr)); } catch (Exception ex) { throw new ValidationErrorException("Invalid value for StackStatus " + statusFilterStr); } } } // TODO: support next token List<StackEntity> stackEntities = StackEntityManager.listStacks(accountId, statusFilterList); ArrayList<StackSummary> stackSummaryList = new ArrayList<StackSummary>(); for ( final StackEntity stackEntity : Iterables.filter( stackEntities, RestrictedTypes.filterPrivileged( ) ) ) { StackSummary stackSummary = new StackSummary(); stackSummary.setCreationTime(stackEntity.getCreateOperationTimestamp()); stackSummary.setDeletionTime(stackEntity.getDeleteOperationTimestamp()); stackSummary.setLastUpdatedTime(stackEntity.getLastUpdateOperationTimestamp()); stackSummary.setStackId(stackEntity.getStackId()); stackSummary.setStackName(stackEntity.getStackName()); stackSummary.setStackStatus(stackEntity.getStackStatus().toString()); stackSummary.setTemplateDescription(stackEntity.getDescription()); stackSummaryList.add(stackSummary); } ListStacksResult listStacksResult = new ListStacksResult(); StackSummaries stackSummaries = new StackSummaries(); stackSummaries.setMember(stackSummaryList); listStacksResult.setStackSummaries(stackSummaries); reply.setListStacksResult(listStacksResult); } catch (Exception ex) { handleException(ex); } return reply; } public SetStackPolicyResponseType setStackPolicy(SetStackPolicyType request) throws CloudFormationException { SetStackPolicyResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String accountId = user.getAccountNumber(); // TODO: validate policy final String stackName = request.getStackName(); final String stackPolicyBody = request.getStackPolicyBody(); final String stackPolicyUrl = request.getStackPolicyURL(); if (stackName == null) throw new ValidationErrorException("Stack name is null"); final String stackPolicyText = validateAndGetStackPolicy(user, stackPolicyBody, stackPolicyUrl); // body could be null (?) (i.e. remove policy) StackEntity stackEntity = StackEntityManager.getAnyStackByNameOrId( stackName, ctx.isAdministrator() && stackName.startsWith(STACK_ID_PREFIX) ? null : accountId); if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } if ( !RestrictedTypes.filterPrivileged( ).apply( stackEntity ) ) { throw new AccessDeniedException( "Not authorized." ); } stackEntity.setStackPolicy(stackPolicyText); StackEntityManager.updateStack(stackEntity); } catch (Exception ex) { handleException(ex); } return reply; } private String validateAndGetStackPolicy(User user, String stackPolicyBody, String stackPolicyUrl) throws ValidationErrorException { if (stackPolicyBody != null && stackPolicyUrl != null) throw new ValidationErrorException("You cannot specify both StackPolicyURL and StackPolicyBody"); if (stackPolicyBody != null) { if (stackPolicyBody.getBytes().length > Limits.REQUEST_STACK_POLICY_MAX_CONTENT_LENGTH_BYTES) { throw new ValidationErrorException("StackPolicy body may not exceed " + Limits.REQUEST_STACK_POLICY_MAX_CONTENT_LENGTH_BYTES + " bytes in a request."); } } return (stackPolicyBody != null) ? stackPolicyBody : (stackPolicyUrl != null ? extractStackPolicyFromURL(stackPolicyUrl, user) : null); } private String validateAndGetStackPolicyDuringUpdate(User user, String stackPolicyDuringUpdateBody, String stackPolicyDuringUpdateUrl) throws ValidationErrorException { if (stackPolicyDuringUpdateBody != null && stackPolicyDuringUpdateUrl != null) throw new ValidationErrorException("You cannot specify both StackPolicyDuringUpdateURL and StackPolicyDuringUpdateBody"); if (stackPolicyDuringUpdateBody != null) { if (stackPolicyDuringUpdateBody.getBytes().length > Limits.REQUEST_STACK_POLICY_MAX_CONTENT_LENGTH_BYTES) { throw new ValidationErrorException("StackPolicy body may not exceed " + Limits.REQUEST_STACK_POLICY_MAX_CONTENT_LENGTH_BYTES + " bytes in a request."); } } return (stackPolicyDuringUpdateBody != null) ? stackPolicyDuringUpdateBody : (stackPolicyDuringUpdateUrl != null ? extractStackPolicyDuringUpdateFromURL(stackPolicyDuringUpdateUrl, user) : null); } public SignalResourceResponseType signalResource(SignalResourceType request) throws CloudFormationException { SignalResourceResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = user.getAccountNumber(); final String accountAlias = ctx.getAccountAlias(); final String stackName = request.getStackName(); final String logicalResourceId = request.getLogicalResourceId(); final String status = request.getStatus(); final String uniqueId = request.getUniqueId(); if (stackName == null) { throw new ValidationErrorException("StackName must not be null"); } if (logicalResourceId == null) { throw new ValidationErrorException("LogicalResourceId must not be null"); } if (uniqueId == null) { throw new ValidationErrorException("UniqueId must not be null"); } if (status == null) { throw new ValidationErrorException("Status must not be null"); } if (!"SUCCESS".equals(status) && !"FAILURE".equals(status)) { throw new ValidationErrorException("Status must either be SUCCESS or FAILURE"); } checkStackPermission( ctx, stackName, accountId, true ); final StackEntity stackEntity = StackEntityManager.getNonDeletedStackByNameOrId( stackName, accountId); // no administrator check here because signal requires user on account stack. if (stackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } final String stackId = stackEntity.getStackId(); // status check if (stackEntity.getStackStatus() != Status.CREATE_IN_PROGRESS && stackEntity.getStackStatus() != Status.UPDATE_IN_PROGRESS && stackEntity.getStackStatus() != Status.UPDATE_ROLLBACK_IN_PROGRESS) { // throw new ValidationErrorException("Stack:" + stackId + " is in " + stackEntity.getStackStatus().toString() + " state and can not be signaled."); } final StackResourceEntity stackResourceEntity = StackResourceEntityManager.getStackResource(stackId, accountId, logicalResourceId, stackEntity.getStackVersion()); if (stackResourceEntity == null) { throw new ValidationErrorException("Resource " + logicalResourceId + " does not exist for stack " + stackName); } ResourceInfo resourceInfo = StackResourceEntityManager.getResourceInfo(stackResourceEntity); if (!resourceInfo.supportsSignals()) { throw new ValidationErrorException("Resource " + logicalResourceId + " is of type " + resourceInfo.getType() + " and cannot be signaled"); } if (stackResourceEntity.getResourceStatus() != Status.CREATE_IN_PROGRESS && stackResourceEntity.getResourceStatus() != Status.UPDATE_IN_PROGRESS) { throw new ValidationErrorException("Resource " + logicalResourceId + " is in " + stackResourceEntity.getResourceStatus().toString() + " state and can not be signaled."); } SignalEntity signal = SignalEntityManager.getSignal(stackId, accountId, logicalResourceId, stackResourceEntity.getResourceVersion(), uniqueId); if (signal != null && !"FAILURE".equals(status)) { throw new ValidationErrorException("Signal with ID " + uniqueId + " for resource " + logicalResourceId+ " already exists. Signals may only be updated with a FAILURE status."); } if (signal != null) { signal.setStatus(SignalEntity.Status.valueOf(status)); signal.setProcessed(false); SignalEntityManager.updateSignal(signal); } else { signal = new SignalEntity(); signal.setStackId(stackId); signal.setAccountId(accountId); signal.setLogicalResourceId(logicalResourceId); signal.setResourceVersion(stackResourceEntity.getResourceVersion()); signal.setUniqueId(uniqueId); signal.setStatus(SignalEntity.Status.valueOf(status)); SignalEntityManager.addSignal(signal); } SignalResourceResult signalResourceResult = new SignalResourceResult(); reply.setSignalResourceResult(signalResourceResult); } catch (Exception ex) { handleException(ex); } return reply; } public UpdateStackResponseType updateStack(UpdateStackType request) throws CloudFormationException { UpdateStackResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = user.getAccountNumber(); final String accountAlias = ctx.getAccountAlias(); // TODO: validate policy String stackName = request.getStackName(); if (stackName == null) { throw new ValidationErrorException("StackName must not be null"); } List<Parameter> nextParameters = null; if (request.getParameters() != null && request.getParameters().getMember() != null) { nextParameters = request.getParameters().getMember(); } final ArrayList<String> nextCapabilities = Lists.newArrayList(); if (request.getCapabilities() != null && request.getCapabilities().getMember() != null) { for (String nextCapability: request.getCapabilities().getMember()) { TemplateParser.Capabilities nextCapabilityEnum = null; try { nextCapabilityEnum = TemplateParser.Capabilities.valueOf(nextCapability); } catch (Exception ex) { } if (nextCapabilityEnum == null) { throw new ValidationErrorException("Capability " + nextCapability + " is not a valid capability. Valid values are " + Lists.newArrayList(TemplateParser.Capabilities.values())); } nextCapabilities.add(nextCapability); } } final String nextStackPolicyBody = request.getStackPolicyBody(); final String nextStackPolicyUrl = request.getStackPolicyURL(); final String nextStackPolicyText = validateAndGetStackPolicy(user, nextStackPolicyBody, nextStackPolicyUrl); final String tempStackPolicyBody = request.getStackPolicyDuringUpdateBody(); final String tempStackPolicyUrl = request.getStackPolicyDuringUpdateURL(); final String tempStackPolicyText = validateAndGetStackPolicyDuringUpdate(user, tempStackPolicyBody, tempStackPolicyUrl); final String nextTemplateBody = request.getTemplateBody(); if (nextTemplateBody != null) { if (nextTemplateBody.getBytes().length > Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES) { throw new ValidationErrorException("Template body may not exceed " + Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES + " bytes in a request."); } } final String nextTemplateUrl = request.getTemplateURL(); final boolean usePreviousTemplate = (request.getUsePreviousTemplate() == null) ? false : request.getUsePreviousTemplate().booleanValue(); if (usePreviousTemplate && (nextTemplateBody != null || nextTemplateUrl != null)) { throw new ValidationErrorException("You cannot specify both usePreviousTemplate and Template Body/Template URL"); } if (nextTemplateBody != null && nextTemplateUrl != null) throw new ValidationErrorException("You cannot specify both Template Body and Template URL"); if (!usePreviousTemplate && (nextTemplateBody == null && nextTemplateUrl == null)) { throw new ValidationErrorException("You must specify either Template Body or Template URL"); } checkStackPermission( ctx, stackName, accountId ); // get the original stack (needed for many things) final StackEntity previousStackEntity = StackEntityManager.getNonDeletedStackByNameOrId( stackName, accountId); // no administrator check here because update requires user on original account stack. if (previousStackEntity == null) { throw new ValidationErrorException("Stack " + stackName + " does not exist"); } final String stackId = previousStackEntity.getStackId(); // make sure stack name is REALLY stack name going forward. (Nested stacks pass stackId) stackName = previousStackEntity.getStackName(); // just a quick check here (no need to check parameters yet) if (previousStackEntity.getStackStatus() != Status.CREATE_COMPLETE && previousStackEntity.getStackStatus() != Status.UPDATE_COMPLETE && previousStackEntity.getStackStatus() != Status.UPDATE_ROLLBACK_COMPLETE) { throw new ValidationErrorException("Stack:" + stackId + " is in " + previousStackEntity.getStackStatus().toString() + " state and can not be updated."); } int previousStackVersion = previousStackEntity.getStackVersion(); if (request.getTags()!= null && request.getTags().getMember() != null) { for (Tag tag: request.getTags().getMember()) { if (Strings.isNullOrEmpty(tag.getKey()) || Strings.isNullOrEmpty(tag.getValue())) { throw new ValidationErrorException("Tags can not be null or empty"); } else if (tag.getKey().startsWith("aws:") ) { throw new ValidationErrorException("Invalid tag key. \"aws:\" is a reserved prefix."); } else if (tag.getKey().startsWith("euca:") ) { throw new ValidationErrorException("Invalid tag key. \"euca:\" is a reserved prefix."); } } } final PseudoParameterValues nextPseudoParameterValues = new PseudoParameterValues(); nextPseudoParameterValues.setAccountId(accountId); nextPseudoParameterValues.setStackName(stackName); nextPseudoParameterValues.setStackId(stackId); ArrayList<String> nextNotificationArns = null; if (request.getNotificationARNs() != null && request.getNotificationARNs().getMember() != null) { nextNotificationArns = Lists.newArrayList(); for (String notificationArn: request.getNotificationARNs().getMember()) { nextNotificationArns.add(notificationArn); } nextPseudoParameterValues.setNotificationArns(nextNotificationArns); } nextPseudoParameterValues.setRegion(getRegion()); final String nextTemplateText = (usePreviousTemplate ? previousStackEntity.getTemplateBody() : (nextTemplateBody != null) ? nextTemplateBody : extractTemplateTextFromURL(nextTemplateUrl, user)); final List<Parameter> previousParameters = convertToParameters(StackEntityHelper.jsonToParameters(previousStackEntity.getParametersJson())); validateAndUpdateParameters(previousParameters, nextParameters); // check that nothing has changed (within resources) final String previousTemplateText = previousStackEntity.getTemplateBody(); List<String> previousCapabilities = StackEntityHelper.jsonToCapabilities(previousStackEntity.getCapabilitiesJson()); PseudoParameterValues previousPseudoParameterValues = getPseudoParameterValues(previousStackEntity); // don't enforce resource properties on previous template final Template previousTemplate = new TemplateParser().parse(previousTemplateText, previousParameters, previousCapabilities, previousPseudoParameterValues, userId, false); final Template nextTemplate = new TemplateParser().parse(nextTemplateText, nextParameters, nextCapabilities, nextPseudoParameterValues, userId, CloudFormationProperties.ENFORCE_STRICT_RESOURCE_PROPERTIES); // see if any of the resources has changed types (this is a no-no) List<String> changedTypeResources = Lists.newArrayList(); for (String resourceName: previousTemplate.getResourceInfoMap().keySet()) { if (Boolean.TRUE.equals(previousTemplate.getResourceInfoMap().get(resourceName).getAllowedByCondition()) && nextTemplate.getResourceInfoMap().containsKey(resourceName) && Boolean.TRUE.equals(nextTemplate.getResourceInfoMap().get(resourceName).getAllowedByCondition()) && !previousTemplate.getResourceInfoMap().get(resourceName).getType().equals(nextTemplate.getResourceInfoMap().get(resourceName).getType())) { changedTypeResources.add(resourceName); } } if (!changedTypeResources.isEmpty()) { throw new ValidationErrorException("Update of resource type is not permitted. The new template modifies resource type of the following resources: " + changedTypeResources); } boolean requiresUpdate = false; // Things that can trigger update. // 1) Changes to Notification ARN. Experimentation shows order doesn't matter but multiplicity does. Use Multisets Multiset<String> previousNotificationArnsMS = HashMultiset.create(); List<String> previousNotificationArns = StackEntityHelper.jsonToNotificationARNs(previousStackEntity.getNotificationARNsJson()); if (previousNotificationArns != null) { previousNotificationArnsMS.addAll(previousNotificationArns); } Multiset<String> nextNotificationArnsMS = HashMultiset.create(); if (nextPseudoParameterValues.getNotificationArns() != null) { nextNotificationArnsMS.addAll(nextPseudoParameterValues.getNotificationArns()); } if (!previousNotificationArnsMS.equals(nextNotificationArnsMS)) { requiresUpdate = true; } // 2) Changes to Stack Policy (TODO: do something better than this). Field equivalence appears to not be considered a change. else if (stackPolicyIsDifferent(previousStackEntity.getStackPolicy(), nextStackPolicyText)) { requiresUpdate = true; } // 3) Differences in the field names (i.e. new or old fields) else if (!previousTemplate.getResourceInfoMap().keySet().equals(nextTemplate.getResourceInfoMap().keySet())) { requiresUpdate = true; } // 4) changes to tags else if (tagsHaveChanged(request, previousStackEntity)) { requiresUpdate = true; } // 5) Differences in the metadata or properties for a given field else { // Note: Ref: to resources will not work here, nor will Fn::GetAtt calls. However, some items can be evaluated // before hand (like Ref: to parameters). We will attempt to evaluate functions for the metadata and properties // fields. Presumably, however, if a Ref: (resource) or a Fn::GetAtt value changes, it is because a different // resource has also changed, so we will evaluate where we can, and leave the value raw if we can not evaluate all functions. for (String fieldName:previousTemplate.getResourceInfoMap().keySet()) { JsonNode previousMetadataJson = tryEvaluateFunctionsInMetadata(previousTemplate, fieldName, userId); JsonNode nextMetadataJson = tryEvaluateFunctionsInMetadata(nextTemplate, fieldName, userId); if (!equalsJson(previousMetadataJson, nextMetadataJson)) { requiresUpdate = true; break; } JsonNode previousPropertiesJson = tryEvaluateFunctionsInProperties(previousTemplate, fieldName, userId); JsonNode nextPropertiesJson = tryEvaluateFunctionsInProperties(nextTemplate, fieldName, userId); if (!equalsJson(previousPropertiesJson, nextPropertiesJson)) { requiresUpdate = true; break; } } } // 6) If this is an "outer" stack (that contains a nested stack) always update for (ResourceInfo resourceInfo: nextTemplate.getResourceInfoMap().values()) { if (Boolean.TRUE.equals(resourceInfo.getAllowedByCondition()) && resourceInfo.getType().equals("AWS::CloudFormation::Stack")) { requiresUpdate = true; break; } } if (!requiresUpdate) { throw new ValidationErrorException(NO_UPDATES_ARE_TO_BE_PERFORMED); } // don't add the record until we check the stack status though and update it. final StackEntity nextStackEntity = StackEntityManager.checkValidUpdateStatusAndUpdateStack(stackId, accountId, nextTemplate, nextTemplateText, request, previousStackVersion); String outerStackArn = StackResourceEntityManager.findOuterStackArnIfExists(stackId, accountId); // Create the new stack resources for (ResourceInfo resourceInfo: nextTemplate.getResourceInfoMap().values()) { StackResourceEntity stackResourceEntity = new StackResourceEntity(); stackResourceEntity = StackResourceEntityManager.updateResourceInfo(stackResourceEntity, resourceInfo); stackResourceEntity.setDescription(""); // TODO: maybe on resource info? stackResourceEntity.setResourceStatus(Status.NOT_STARTED); stackResourceEntity.setStackId(stackId); stackResourceEntity.setStackName(stackName); stackResourceEntity.setRecordDeleted(Boolean.FALSE); stackResourceEntity.setResourceVersion(nextStackEntity.getStackVersion()); StackResourceEntityManager.addStackResource(stackResourceEntity); } String previousResourceDependencyManagerJson = StackEntityHelper.resourceDependencyManagerToJson(previousTemplate.getResourceDependencyManager()); StackUpdateInfoEntityManager.createUpdateInfo(stackId, accountId, previousResourceDependencyManagerJson, nextStackEntity.getResourceDependencyManagerJson(), nextStackEntity.getStackVersion(), stackName, accountAlias); StackWorkflowTags stackWorkflowTags = new StackWorkflowTags(stackId, stackName, accountId, accountAlias); StartTimeoutPassableWorkflowClientFactory updateStackWorkflowClientFactory = new StartTimeoutPassableWorkflowClientFactory(WorkflowClientManager.getSimpleWorkflowClient( ), CloudFormationProperties.SWF_DOMAIN, CloudFormationProperties.SWF_TASKLIST); WorkflowDescriptionTemplate updateStackWorkflowDescriptionTemplate = new UpdateStackWorkflowDescriptionTemplate(); InterfaceBasedWorkflowClient<UpdateStackWorkflow> updateStackWorkflowClient = updateStackWorkflowClientFactory .getNewWorkflowClient(UpdateStackWorkflow.class, updateStackWorkflowDescriptionTemplate, stackWorkflowTags, null, null); UpdateStackWorkflow updateStackWorkflow = new UpdateStackWorkflowClient(updateStackWorkflowClient); updateStackWorkflow.updateStack(nextStackEntity.getStackId(), nextStackEntity.getAccountId(), nextStackEntity.getResourceDependencyManagerJson(), userId, nextStackEntity.getStackVersion()); StackWorkflowEntityManager.addOrUpdateStackWorkflowEntity(stackId, StackWorkflowEntity.WorkflowType.UPDATE_STACK_WORKFLOW, CloudFormationProperties.SWF_DOMAIN, updateStackWorkflowClient.getWorkflowExecution().getWorkflowId(), updateStackWorkflowClient.getWorkflowExecution().getRunId()); StartTimeoutPassableWorkflowClientFactory monitorUpdateStackWorkflowClientFactory = new StartTimeoutPassableWorkflowClientFactory(WorkflowClientManager.getSimpleWorkflowClient(), CloudFormationProperties.SWF_DOMAIN, CloudFormationProperties.SWF_TASKLIST); WorkflowDescriptionTemplate monitorUpdateStackWorkflowDescriptionTemplate = new MonitorUpdateStackWorkflowDescriptionTemplate(); InterfaceBasedWorkflowClient<MonitorUpdateStackWorkflow> monitorUpdateStackWorkflowClient = monitorUpdateStackWorkflowClientFactory .getNewWorkflowClient(MonitorUpdateStackWorkflow.class, monitorUpdateStackWorkflowDescriptionTemplate, stackWorkflowTags, null, null); MonitorUpdateStackWorkflow monitorUpdateStackWorkflow = new MonitorUpdateStackWorkflowClient(monitorUpdateStackWorkflowClient); monitorUpdateStackWorkflow.monitorUpdateStack(nextStackEntity.getStackId(), nextStackEntity.getAccountId(), userId, nextStackEntity.getStackVersion(), outerStackArn); StackWorkflowEntityManager.addOrUpdateStackWorkflowEntity(stackId, StackWorkflowEntity.WorkflowType.MONITOR_UPDATE_STACK_WORKFLOW, CloudFormationProperties.SWF_DOMAIN, monitorUpdateStackWorkflowClient.getWorkflowExecution().getWorkflowId(), monitorUpdateStackWorkflowClient.getWorkflowExecution().getRunId()); UpdateStackResult updateStackResult = new UpdateStackResult(); updateStackResult.setStackId(stackId); reply.setUpdateStackResult(updateStackResult); } catch (Exception ex) { handleException(ex); } return reply; } private boolean tagsHaveChanged(UpdateStackType request, StackEntity previousStackEntity) throws CloudFormationException { Map<String, String> previousTagsMap = Maps.newHashMap(); Map<String, String> nextTagsMap = Maps.newHashMap(); if (request.getTags() !=null && request.getTags().getMember() != null) { for (Tag tag : request.getTags().getMember()) { nextTagsMap.put(tag.getKey(), tag.getValue()); } List<Tag> previousTags = StackEntityHelper.jsonToTags(previousStackEntity.getTagsJson()); for (Tag tag: previousTags) { previousTagsMap.put(tag.getKey(), tag.getValue()); } if (!previousTagsMap.equals(nextTagsMap)) { return true; } } return false; } private JsonNode tryEvaluateFunctionsInMetadata(Template template, String fieldName, String userId) throws CloudFormationException { JsonNode metadataJson = JsonHelper.getJsonNodeFromString(template.getResourceInfoMap().get(fieldName).getMetadataJson()); metadataJson = FunctionEvaluation.evaluateFunctionsPreResourceResolution(metadataJson, template, userId); return metadataJson; } private JsonNode tryEvaluateFunctionsInProperties(Template template, String fieldName, String userId) throws CloudFormationException { JsonNode propertiesJson = JsonHelper.getJsonNodeFromString(template.getResourceInfoMap().get(fieldName).getPropertiesJson()); propertiesJson = FunctionEvaluation.evaluateFunctionsPreResourceResolution(propertiesJson, template, userId); return propertiesJson; } private boolean equalsJson(JsonNode node1, JsonNode node2) { if (node1 == null && node2 == null) return true; // TODO: not sure about this case... if (node1 != null && node2 == null) return false; if (node1 == null && node2 != null) return false; return node1.equals(node2); } private boolean stackPolicyIsDifferent(String previousStackPolicy, String nextStackPolicy) throws ValidationErrorException { if (nextStackPolicy == null) return false; if (previousStackPolicy == null && nextStackPolicy != null) return true; JsonNode previousStackPolicyNode; try { previousStackPolicyNode = Json.parse( previousStackPolicy ); } catch (IOException ex) { throw new ValidationErrorException("Current stack policy is invalid"); } if (!previousStackPolicyNode.isObject()) { throw new ValidationErrorException("Current stack policy is invalid"); } JsonNode nextStackPolicyNode; try { nextStackPolicyNode = Json.parse( nextStackPolicy ); } catch (IOException ex) { throw new ValidationErrorException("stack policy is invalid"); } if (!nextStackPolicyNode.isObject()) { throw new ValidationErrorException("stack policy is invalid"); } return equalsJsonUnorderedLists(previousStackPolicyNode, nextStackPolicyNode); } private boolean equalsJsonUnorderedLists(JsonNode node1, JsonNode node2) { if (node1 == null && node2 == null) return true; // TODO: not sure about this case... if (node1 != null && node2 == null) return false; if (node1 == null && node2 != null) return false; if (node1.isObject()) { if (!node2.isObject()) return false; Set<String> node1FieldNames = Sets.newHashSet(node1.fieldNames()); Set<String> node2FieldNames = Sets.newHashSet(node2.fieldNames()); if (node1FieldNames == null && node2FieldNames == null) return true; // TODO: not sure about this case... if (node1FieldNames != null && node2FieldNames == null) return false; if (node1FieldNames == null && node2FieldNames != null) return false; for (String fieldName : node1FieldNames) { if (!equalsJsonUnorderedLists(node1.get(fieldName), node2.get(fieldName))) { return false; } } return true; } else if (node1.isArray()) { if (!node2.isArray()) return false; if (node1.size() != node2.size()) return false; // hard to comare array elements as order doesn't matter but no defined way to sort, and multiplicty also matters. // Let's just check if for each element of the first array, there is a corresponding one in the second // Then we can remove and check again. List<JsonNode> node1Elements = Lists.newArrayList(node1.elements()); List<JsonNode> node2Elements = Lists.newArrayList(node2.elements()); Iterator<JsonNode> node1ElementsIter = node1Elements.iterator(); while (node1ElementsIter.hasNext()) { JsonNode node1Element = node1ElementsIter.next(); boolean foundMatchThisTime = false; Iterator<JsonNode> node2ElementsIter = node2Elements.iterator(); while (node2ElementsIter.hasNext()) { if (equalsJsonUnorderedLists(node1Element, node2ElementsIter.next())) { foundMatchThisTime = true; node2ElementsIter.remove(); // remove matching element break; } } if (!foundMatchThisTime) return false; } return true; } else { return node1.asText().equals(node2.asText()); } } private PseudoParameterValues getPseudoParameterValues(VersionedStackEntity stackEntity) throws CloudFormationException { PseudoParameterValues pseudoParameterValues = new PseudoParameterValues(); pseudoParameterValues.setAccountId(stackEntity.getAccountId()); pseudoParameterValues.setStackId(stackEntity.getStackId()); pseudoParameterValues.setStackName(stackEntity.getStackName()); pseudoParameterValues.setNotificationArns(StackEntityHelper.jsonToNotificationARNs(stackEntity.getNotificationARNsJson())); // TODO: make region easier to get? Map<String, String> pseudoParameterMap = StackEntityHelper.jsonToPseudoParameterMap(stackEntity.getPseudoParameterMapJson()); if (pseudoParameterMap.containsKey(TemplateParser.AWS_REGION)) { JsonNode regionJsonNode = JsonHelper.getJsonNodeFromString(pseudoParameterMap.get(TemplateParser.AWS_REGION)); if (regionJsonNode == null || !regionJsonNode.isValueNode()) { throw new ValidationErrorException(TemplateParser.AWS_REGION + " from stack is not a string."); } pseudoParameterValues.setRegion(regionJsonNode.asText()); } return pseudoParameterValues; } private void validateAndUpdateParameters(List<Parameter> previousParameters, List<Parameter> nextParameters) throws ValidationErrorException { Map<String, String> previousParameterMap = Maps.newHashMap(); for (Parameter previousParameter: previousParameters) { previousParameterMap.put(previousParameter.getParameterKey(), previousParameter.getParameterValue()); } if (nextParameters != null) { for (Parameter nextParameter: nextParameters) { if (Boolean.TRUE.equals(nextParameter.getUsePreviousValue())) { if (Strings.isNullOrEmpty(nextParameter.getParameterValue())) { throw new ValidationErrorException("Invalid input for parameter key " + nextParameter.getParameterKey() + ". Cannot specify usePreviousValue as true and non empty value for a parameter."); } if (!previousParameterMap.containsKey(nextParameter.getParameterKey())) { throw new ValidationErrorException("Invalid input for parameter key " + nextParameter.getParameterKey() + ". Cannot specify usePreviousValue as true for a parameter key not in the previous template."); } nextParameter.setParameterValue(previousParameterMap.get(nextParameter.getParameterKey())); } } } } private List<Parameter> convertToParameters(ArrayList<StackEntity.Parameter> stackEntityParameters) { List<Parameter> parameters = Lists.newArrayList(); if (stackEntityParameters != null) { for (StackEntity.Parameter stackEntityParameter : stackEntityParameters) { parameters.add(new Parameter(stackEntityParameter.getKey(), stackEntityParameter.getStringValue())); } } return parameters; } public ValidateTemplateResponseType validateTemplate(ValidateTemplateType request) throws CloudFormationException { ValidateTemplateResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); // IAM Action Check checkActionPermission(CloudFormationPolicySpec.CLOUDFORMATION_VALIDATETEMPLATE, ctx); final User user = ctx.getUser(); final String userId = user.getUserId(); final String accountId = user.getAccountNumber(); final String templateBody = request.getTemplateBody(); final String templateUrl = request.getTemplateURL(); String stackName = "stackName"; // just some value to make the validate code work if (templateBody == null && templateUrl == null) throw new ValidationErrorException("Either TemplateBody or TemplateURL must be set."); if (templateBody != null && templateUrl != null) throw new ValidationErrorException("Exactly one of TemplateBody or TemplateURL must be set."); if (templateBody != null) { if (templateBody.getBytes().length > Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES) { throw new ValidationErrorException("Template body may not exceed " + Limits.REQUEST_TEMPLATE_BODY_MAX_LENGTH_BYTES + " bytes in a request."); } } String templateText = (templateBody != null) ? templateBody : extractTemplateTextFromURL(templateUrl, user); final String stackIdLocal = UUID.randomUUID().toString(); final String stackId = "arn:aws:cloudformation:" + REGION + ":" + accountId + ":stack/"+stackName+"/"+stackIdLocal; final PseudoParameterValues pseudoParameterValues = new PseudoParameterValues(); pseudoParameterValues.setAccountId(accountId); pseudoParameterValues.setStackName(stackName); pseudoParameterValues.setStackId(stackId); ArrayList<String> notificationArns = Lists.newArrayList(); pseudoParameterValues.setRegion(getRegion()); List<Parameter> parameters = Lists.newArrayList(); final ValidateTemplateResult validateTemplateResult = new TemplateParser().validateTemplate(templateText, parameters, pseudoParameterValues, userId, CloudFormationProperties.ENFORCE_STRICT_RESOURCE_PROPERTIES); reply.setValidateTemplateResult(validateTemplateResult); } catch (Exception ex) { handleException(ex); } return reply; } public static String getRegion( ) { return Optional.fromNullable( Strings.emptyToNull( REGION ) ) .or( RegionConfigurations.getRegionName( ) ) .or( "eucalyptus" ); } private static void handleException(final Exception e) throws CloudFormationException { final CloudFormationException cause = Exceptions.findCause(e, CloudFormationException.class); if (cause != null) { throw cause; } LOG.error( e, e ); final InternalFailureException exception = new InternalFailureException( String.valueOf(e.getMessage())); if (Contexts.lookup().hasAdministrativePrivileges()) { exception.initCause(e); } throw exception; } private static void checkStackPermission( Context ctx, String stackName, String accountId ) throws AccessDeniedException { checkStackPermission( ctx, stackName, accountId, false ); } private static void checkStackPermission( @Nonnull final Context ctx, @Nonnull final String stackName, @Nullable final String accountId, final boolean allowInstance ) throws AccessDeniedException { StackEntity stackEntity = StackEntityManager.getAnyStackByNameOrId(stackName, accountId); if ( stackEntity == null && ctx.isAdministrator( ) && stackName.startsWith( STACK_ID_PREFIX ) ) { stackEntity = StackEntityManager.getAnyStackByNameOrId(stackName, null); } if ( stackEntity != null && !RestrictedTypes.filterPrivileged().apply( stackEntity ) ) { boolean instanceAccessPermitted = false; if ( allowInstance && ctx.getSubject( ) != null ) { final Set<CfnIdentityDocumentCredential> credentials = ctx.getSubject( ).getPublicCredentials( CfnIdentityDocumentCredential.class ); if ( !credentials.isEmpty( ) ) { final String instanceId = Iterables.get( credentials, 0 ).getInstanceId( ); instanceAccessPermitted = !StackResourceEntityManager .describeStackResources( accountId, stackName, instanceId, null ).isEmpty( ); } } if ( !instanceAccessPermitted ) { throw new AccessDeniedException( "Not authorized." ); } } } private static void checkActionPermission(final String actionType, final Context ctx) throws AccessDeniedException { if (!Permissions.isAuthorized(CloudFormationPolicySpec.VENDOR_CLOUDFORMATION, actionType, "", ctx.getAccount(), actionType, ctx.getAuthContext())) { throw new AccessDeniedException("Not authorized."); } } }