Java tutorial
/************************************************************************* * 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.WorkflowExecution; import com.amazonaws.services.simpleworkflow.model.WorkflowExecutionDetail; 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.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.StackWorkflowEntity; import com.eucalyptus.cloudformation.entity.StackWorkflowEntityManager; import com.eucalyptus.cloudformation.resources.ResourceInfo; 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.workflow.CreateStackWorkflow; import com.eucalyptus.cloudformation.workflow.CreateStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.CreateStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.DeleteStackWorkflow; import com.eucalyptus.cloudformation.workflow.DeleteStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.DeleteStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.MonitorCreateStackWorkflow; import com.eucalyptus.cloudformation.workflow.MonitorCreateStackWorkflowClient; import com.eucalyptus.cloudformation.workflow.MonitorCreateStackWorkflowDescriptionTemplate; import com.eucalyptus.cloudformation.workflow.StartTimeoutPassableWorkflowClientFactory; import com.eucalyptus.cloudformation.workflow.WorkflowClientManager; import com.eucalyptus.cloudformation.ws.StackWorkflowTags; import com.eucalyptus.component.ComponentIds; 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.RestrictedTypes; import com.eucalyptus.util.dns.DomainNames; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; import com.netflix.glisten.InterfaceBasedWorkflowClient; import com.netflix.glisten.WorkflowClientFactory; import com.netflix.glisten.WorkflowDescriptionTemplate; import org.apache.commons.io.input.BoundedInputStream; import org.apache.log4j.Logger; import org.xbill.DNS.Name; 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.Date; import java.util.List; import java.util.UUID; import javax.net.ssl.SSLHandshakeException; @ConfigurableClass(root = "cloudformation", description = "Parameters controlling cloud formation") public class CloudFormationService { @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 { return request.getReply(); } 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 accountName = ctx.getAccountAlias(); final String stackName = request.getStackName(); final String templateBody = request.getTemplateBody(); final String templateUrl = request.getTemplateURL(); 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) { capabilities.addAll(request.getCapabilities().getMember()); } 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); final Supplier<StackEntity> allocator = new Supplier<StackEntity>() { @Override public StackEntity get() { try { StackEntity stackEntity = new StackEntity(); StackEntityHelper.populateStackEntityWithTemplate(stackEntity, template); stackEntity.setStackName(stackName); stackEntity.setStackId(stackId); stackEntity.setNaturalId(stackIdLocal); stackEntity.setAccountId(accountId); stackEntity.setTemplateBody(templateText); stackEntity.setStackStatus(StackEntity.Status.CREATE_IN_PROGRESS); stackEntity.setStackStatusReason("User initiated"); stackEntity.setDisableRollback(request.getDisableRollback() == Boolean.TRUE); // 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.setRecordDeleted(Boolean.FALSE); 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 = (request.getDisableRollback() == Boolean.TRUE) ? "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(StackResourceEntity.Status.NOT_STARTED); stackResourceEntity.setStackId(stackId); stackResourceEntity.setStackName(stackName); stackResourceEntity.setRecordDeleted(Boolean.FALSE); StackResourceEntityManager.addStackResource(stackResourceEntity); } StackWorkflowTags stackWorkflowTags = new StackWorkflowTags(stackId, stackName, accountId, accountName); 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); StackWorkflowEntityManager.addOrUpdateStackWorkflowEntity(stackId, StackWorkflowEntity.WorkflowType.CREATE_STACK_WORKFLOW, CloudFormationProperties.SWF_DOMAIN, createStackWorkflowClient.getWorkflowExecution().getWorkflowId(), createStackWorkflowClient.getWorkflowExecution().getRunId()); WorkflowClientFactory monitorCreateStackWorkflowClientFactory = new WorkflowClientFactory( WorkflowClientManager.getSimpleWorkflowClient(), CloudFormationProperties.SWF_DOMAIN, CloudFormationProperties.SWF_TASKLIST); WorkflowDescriptionTemplate monitorCreateStackWorkflowDescriptionTemplate = new MonitorCreateStackWorkflowDescriptionTemplate(); InterfaceBasedWorkflowClient<MonitorCreateStackWorkflow> monitorCreateStackWorkflowClient = monitorCreateStackWorkflowClientFactory .getNewWorkflowClient(MonitorCreateStackWorkflow.class, monitorCreateStackWorkflowDescriptionTemplate, stackWorkflowTags); MonitorCreateStackWorkflow monitorCreateStackWorkflow = new MonitorCreateStackWorkflowClient( monitorCreateStackWorkflowClient); monitorCreateStackWorkflow.monitorCreateStack(stackEntity.getStackId(), stackEntity.getAccountId(), stackEntity.getResourceDependencyManagerJson(), userId, onFailure); 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); } } }; final StackEntity stackEntity = RestrictedTypes.allocateUnitlessResource(allocator); CreateStackResult createStackResult = new CreateStackResult(); createStackResult.setStackId(stackId); reply.setCreateStackResult(createStackResult); } catch (Exception ex) { handleException(ex); } return reply; } private static String extractTemplateTextFromURL(String templateUrl, User user) throws ValidationErrorException { final URL url; try { url = new URL(templateUrl); } catch (MalformedURLException e) { throw new ValidationErrorException("Invalid template url " + templateUrl); } // First try straight HTTP GET if url is in whitelist boolean inWhitelist = WhiteListURLMatcher.urlIsAllowed(url, URL_DOMAIN_WHITELIST); if (inWhitelist) { InputStream templateIn = null; try { final URLConnection connection = SslSetup.configureHttpsUrlConnection(url.openConnection()); templateIn = connection.getInputStream(); long contentLength = connection.getContentLengthLong(); if (contentLength > Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES) { throw new ValidationErrorException("Template URL exceeds maximum byte count, " + Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES); } final byte[] templateData = ByteStreams.toByteArray(new BoundedInputStream(templateIn, Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES + 1)); if (templateData.length > Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES) { throw new ValidationErrorException("Template URL exceeds maximum byte count, " + Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES); } return new String(templateData, StandardCharsets.UTF_8); } catch (UnknownHostException ex) { throw new ValidationErrorException("Invalid template url " + templateUrl); } catch (SSLHandshakeException ex) { throw new ValidationErrorException("HTTPS connection error for " + templateUrl); } catch (IOException ex) { if (Strings.nullToEmpty(ex.getMessage()).startsWith("HTTPS hostname wrong")) { throw new ValidationErrorException( "HTTPS connection failed hostname verification for " + templateUrl); } 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(new SecurityTokenAWSCredentialsProvider(user))) { if (eucaS3Client.getObjectMetadata(bucketAndKey.getBucket(), bucketAndKey.getKey()) .getContentLength() > Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES) { throw new ValidationErrorException("Template URL exceeds maximum byte count, " + Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES); } return eucaS3Client.getObjectContent(bucketAndKey.getBucket(), bucketAndKey.getKey(), (int) Limits.REQUEST_TEMPLATE_URL_MAX_CONTENT_LENGTH_BYTES); } catch (Exception ex) { LOG.debug("Error getting s3 object content: " + bucketAndKey.getBucket() + "/" + bucketAndKey.getKey()); LOG.debug(ex, ex); throw new ValidationErrorException( "Template url is an S3 URL to a non-existent or unauthorized bucket/key. (bucket=" + bucketAndKey.getBucket() + ", key=" + bucketAndKey.getKey()); } } public DeleteStackResponseType deleteStack(final DeleteStackType request) throws CloudFormationException { DeleteStackResponseType 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) { if (!RestrictedTypes.filterPrivileged().apply(stackEntity)) { throw new AccessDeniedException("Not authorized."); } final String stackAccountId = stackEntity.getAccountId(); // check to see if there has been a delete workflow. If one exists and is still going on, just quit: boolean existingOpenDeleteWorkflow = 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; } } catch (Exception ex) { LOG.error("Unable to get status of delete workflow for " + stackEntity.getStackId() + ", assuming not open"); LOG.debug(ex); } } if (!existingOpenDeleteWorkflow) { String stackId = stackEntity.getStackId(); StackWorkflowTags stackWorkflowTags = new StackWorkflowTags(stackId, stackName, stackAccountId, accountAlias); WorkflowClientFactory workflowClientFactory = new WorkflowClientFactory( WorkflowClientManager.getSimpleWorkflowClient(), CloudFormationProperties.SWF_DOMAIN, CloudFormationProperties.SWF_TASKLIST); WorkflowDescriptionTemplate workflowDescriptionTemplate = new DeleteStackWorkflowDescriptionTemplate(); InterfaceBasedWorkflowClient<DeleteStackWorkflow> client = workflowClientFactory .getNewWorkflowClient(DeleteStackWorkflow.class, workflowDescriptionTemplate, stackWorkflowTags); DeleteStackWorkflow deleteStackWorkflow = new DeleteStackWorkflowClient(client); deleteStackWorkflow.deleteStack(stackId, stackAccountId, stackEntity.getResourceDependencyManagerJson(), userId); StackWorkflowEntityManager.addOrUpdateStackWorkflowEntity(stackEntity.getStackId(), StackWorkflowEntity.WorkflowType.DELETE_STACK_WORKFLOW, CloudFormationProperties.SWF_DOMAIN, client.getWorkflowExecution().getWorkflowId(), client.getWorkflowExecution().getRunId()); } } } catch (Exception ex) { handleException(ex); } return reply; } 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); 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); 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<StackEntity.Status> statusFilterList = Lists.newArrayList(); if (stackStatusFilter != null && stackStatusFilter.getMember() != null) { for (String statusFilterStr : stackStatusFilter.getMember()) { try { statusFilterList.add(StackEntity.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(); if (request.getStackPolicyURL() != null) { throw new ValidationErrorException("StackPolicyURL is not supported"); } if (stackName == null) throw new ValidationErrorException("Stack name is null"); // 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(stackPolicyBody); StackEntityManager.updateStack(stackEntity); } catch (Exception ex) { handleException(ex); } return reply; } public SignalResourceResponseType signalResource(SignalResourceType request) throws CloudFormationException { return request.getReply(); } public UpdateStackResponseType updateStack(UpdateStackType request) throws CloudFormationException { return request.getReply(); } 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); 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 { 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)) { 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."); } } }