Commits

Steffen Opel  committed b1f1266 Merge

BAWS-68: Merged branch default.

  • Participants
  • Parent commits e7c4e38, ba3cc77
  • Branches BAWS-68

Comments (0)

Files changed (17)

   <classpathentry kind="src" path="src/test/java" output="target/test-classes" including="**/*.java"/>
   <classpathentry kind="src" path="src/main/java" including="**/*.java"/>
   <classpathentry kind="src" path="src/main/resources" excluding="**/*.java"/>
+  <classpathentry kind="src" path="src/external" including="**/aws.json" excluding="**/*.java"/>
   <classpathentry kind="output" path="target/classes"/>
-  <classpathentry kind="var" path="M2_REPO/javax/activation/activation/1.1.1/activation-1.1.1.jar" sourcepath="M2_REPO/javax/activation/activation/1.1.1/activation-1.1.1-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/javax/activation/activation/1.1/activation-1.1.jar" sourcepath="M2_REPO/javax/activation/activation/1.1/activation-1.1-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/javax/xml/bind/jaxb-api/2.1/jaxb-api-2.1.jar" sourcepath="M2_REPO/javax/xml/bind/jaxb-api/2.1/jaxb-api-2.1-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/javax/jms/jms/1.1/jms-1.1.jar"/>
   <classpathentry kind="var" path="M2_REPO/javax/servlet/jsp/jsp-api/2.1/jsp-api-2.1.jar" sourcepath="M2_REPO/javax/servlet/jsp/jsp-api/2.1/jsp-api-2.1-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/javax/transaction/jta/1.0.1b/jta-1.0.1B.jar"/>
-  <classpathentry kind="var" path="M2_REPO/javax/mail/mail/1.4.1/mail-1.4.1.jar" sourcepath="M2_REPO/javax/mail/mail/1.4.1/mail-1.4.1-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/javax/mail/mail/1.4/mail-1.4.jar" sourcepath="M2_REPO/javax/mail/mail/1.4/mail-1.4-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/javax/servlet/servlet-api/2.5/servlet-api-2.5.jar" sourcepath="M2_REPO/javax/servlet/servlet-api/2.5/servlet-api-2.5-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/javax/xml/stream/stax-api/1.0-2/stax-api-1.0-2.jar" sourcepath="M2_REPO/javax/xml/stream/stax-api/1.0-2/stax-api-1.0-2-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/org/acegisecurity/acegi-security/1.0.4/acegi-security-1.0.4.jar" sourcepath="M2_REPO/org/acegisecurity/acegi-security/1.0.4/acegi-security-1.0.4-sources.jar">
       <attribute value="jar:file:/C:/Cache/maven/repository/commons-cli/commons-cli/1.0/commons-cli-1.0-javadoc.jar!/" name="javadoc_location"/>
     </attributes>
   </classpathentry>
-  <classpathentry kind="var" path="M2_REPO/commons-codec/commons-codec/1.4/commons-codec-1.4.jar" sourcepath="M2_REPO/commons-codec/commons-codec/1.4/commons-codec-1.4-sources.jar">
+  <classpathentry kind="var" path="M2_REPO/commons-codec/commons-codec/1.3/commons-codec-1.3.jar" sourcepath="M2_REPO/commons-codec/commons-codec/1.3/commons-codec-1.3-sources.jar">
     <attributes>
-      <attribute value="jar:file:/C:/Cache/maven/repository/commons-codec/commons-codec/1.4/commons-codec-1.4-javadoc.jar!/" name="javadoc_location"/>
+      <attribute value="jar:file:/C:/Cache/maven/repository/commons-codec/commons-codec/1.3/commons-codec-1.3-javadoc.jar!/" name="javadoc_location"/>
     </attributes>
   </classpathentry>
   <classpathentry kind="var" path="M2_REPO/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar" sourcepath="M2_REPO/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1-sources.jar">
       <attribute value="jar:file:/C:/Cache/maven/repository/commons-lang/commons-lang/2.5/commons-lang-2.5-javadoc.jar!/" name="javadoc_location"/>
     </attributes>
   </classpathentry>
-  <classpathentry kind="var" path="M2_REPO/commons-logging/commons-logging/1.0.4/commons-logging-1.0.4.jar" sourcepath="M2_REPO/commons-logging/commons-logging/1.0.4/commons-logging-1.0.4-sources.jar">
+  <classpathentry kind="var" path="M2_REPO/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.jar" sourcepath="M2_REPO/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1-sources.jar">
     <attributes>
-      <attribute value="jar:file:/C:/Cache/maven/repository/commons-logging/commons-logging/1.0.4/commons-logging-1.0.4-javadoc.jar!/" name="javadoc_location"/>
+      <attribute value="jar:file:/C:/Cache/maven/repository/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1-javadoc.jar!/" name="javadoc_location"/>
     </attributes>
   </classpathentry>
   <classpathentry kind="var" path="M2_REPO/commons-pool/commons-pool/1.4-RC2-atlassian-1/commons-pool-1.4-RC2-atlassian-1.jar" sourcepath="M2_REPO/commons-pool/commons-pool/1.4-RC2-atlassian-1/commons-pool-1.4-RC2-atlassian-1-sources.jar">
   </classpathentry>
   <classpathentry kind="var" path="M2_REPO/hibernate/hibernate/2.1.8-atlassian-9/hibernate-2.1.8-atlassian-9.jar" sourcepath="M2_REPO/hibernate/hibernate/2.1.8-atlassian-9/hibernate-2.1.8-atlassian-9-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/hsqldb/hsqldb/1.8.0.7/hsqldb-1.8.0.7.jar"/>
+  <classpathentry kind="var" path="M2_REPO/org/apache/httpcomponents/httpclient/4.1.1/httpclient-4.1.1.jar" sourcepath="M2_REPO/org/apache/httpcomponents/httpclient/4.1.1/httpclient-4.1.1-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/org/apache/httpcomponents/httpcore/4.2-alpha2/httpcore-4.2-alpha2.jar" sourcepath="M2_REPO/org/apache/httpcomponents/httpcore/4.2-alpha2/httpcore-4.2-alpha2-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/com/ibm/icu/icu4j/3.8/icu4j-3.8.jar"/>
   <classpathentry kind="var" path="M2_REPO/isorelax/isorelax/20020414/isorelax-20020414.jar" sourcepath="M2_REPO/isorelax/isorelax/20020414/isorelax-20020414-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/org/codehaus/jackson/jackson-core-asl/1.9.2/jackson-core-asl-1.9.2.jar" sourcepath="M2_REPO/org/codehaus/jackson/jackson-core-asl/1.9.2/jackson-core-asl-1.9.2-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/org/codehaus/jackson/jackson-mapper-asl/1.9.2/jackson-mapper-asl-1.9.2.jar" sourcepath="M2_REPO/org/codehaus/jackson/jackson-mapper-asl/1.9.2/jackson-mapper-asl-1.9.2-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/org/codehaus/jackson/jackson-mrbean/1.9.2/jackson-mrbean-1.9.2.jar" sourcepath="M2_REPO/org/codehaus/jackson/jackson-mrbean/1.9.2/jackson-mrbean-1.9.2-sources.jar"/>
   <classpathentry kind="var" path="M2_REPO/org/jasypt/jasypt/1.6/jasypt-1.6.jar" sourcepath="M2_REPO/org/jasypt/jasypt/1.6/jasypt-1.6-sources.jar">
     <attributes>
       <attribute value="jar:file:/C:/Cache/maven/repository/org/jasypt/jasypt/1.6/jasypt-1.6-javadoc.jar!/" name="javadoc_location"/>
     </attributes>
   </classpathentry>
   <classpathentry kind="var" path="M2_REPO/jdom/jdom/1.0/jdom-1.0.jar" sourcepath="M2_REPO/jdom/jdom/1.0/jdom-1.0-sources.jar"/>
+  <classpathentry kind="var" path="M2_REPO/com/sun/jersey/contribs/jersey-apache-client4/1.10/jersey-apache-client4-1.10.jar" sourcepath="M2_REPO/com/sun/jersey/contribs/jersey-apache-client4/1.10/jersey-apache-client4-1.10-sources.jar">
+    <attributes>
+      <attribute value="jar:file:/C:/Cache/maven/repository/com/sun/jersey/contribs/jersey-apache-client4/1.10/jersey-apache-client4-1.10-javadoc.jar!/" name="javadoc_location"/>
+    </attributes>
+  </classpathentry>
+  <classpathentry kind="var" path="M2_REPO/com/sun/jersey/jersey-client/1.10/jersey-client-1.10.jar" sourcepath="M2_REPO/com/sun/jersey/jersey-client/1.10/jersey-client-1.10-sources.jar">
+    <attributes>
+      <attribute value="jar:file:/C:/Cache/maven/repository/com/sun/jersey/jersey-client/1.10/jersey-client-1.10-javadoc.jar!/" name="javadoc_location"/>
+    </attributes>
+  </classpathentry>
+  <classpathentry kind="var" path="M2_REPO/com/sun/jersey/jersey-core/1.10/jersey-core-1.10.jar" sourcepath="M2_REPO/com/sun/jersey/jersey-core/1.10/jersey-core-1.10-sources.jar">
+    <attributes>
+      <attribute value="jar:file:/C:/Cache/maven/repository/com/sun/jersey/jersey-core/1.10/jersey-core-1.10-javadoc.jar!/" name="javadoc_location"/>
+    </attributes>
+  </classpathentry>
   <classpathentry kind="var" path="M2_REPO/jfree/jfreechart/1.0.9/jfreechart-1.0.9.jar" sourcepath="M2_REPO/jfree/jfreechart/1.0.9/jfreechart-1.0.9-sources.jar">
     <attributes>
       <attribute value="jar:file:/C:/Cache/maven/repository/jfree/jfreechart/1.0.9/jfreechart-1.0.9-javadoc.jar!/" name="javadoc_location"/>
+src/external/com/github/garnaat/missingcloud = [git]git://github.com/garnaat/missingcloud.git
+cf11c83e10f64af656f3db8dea930dcd403125c9 src/external/com/github/garnaat/missingcloud
 # Overview
 
-The [Bamboo AWS Plugin](https://plugins.atlassian.com/plugin/details/774227) adds a Task to create or delete a collection of related Amazon Web Services resources via AWS CloudFormation.
+The [Bamboo AWS Plugin](https://plugins.atlassian.com/plugin/details/774227) adds a Task to create or delete an AWS CloudFormation stack (a collection of related Amazon Web Services resources).
 
-It allows to unleash the power of AWS from a single Task within your build with the following key features:
+It enables you to unleash the power of AWS from Tasks within your build with the following key features:
 
 * **Stack Lifecycle Management** - create or delete a CloudFormation stack defined by a template provided via URL or inline and specify template parameters and advanced options.
 * **Variable Substitution/Definition** - substitute Bamboo variables in template parameters (and some other fields) to customize the stack and reuse variables defined by stack outputs in subsequent Tasks.
             <version>${bamboo.version}</version>
             <scope>provided</scope>
         </dependency>
-        
+
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-core</artifactId>
+            <version>1.10</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>1.10</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.contribs</groupId>
+            <artifactId>jersey-apache-client4</artifactId>
+            <version>1.10</version>
+        </dependency>
+
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+            <version>1.6</version>
+            <scope>provided</scope>
+        </dependency>
+
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
                 </exclusion>
             </exclusions>
         </dependency>
-    </dependencies>
 
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+            <version>1.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+            <version>1.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mrbean</artifactId>
+            <version>1.9.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.5.8</version>
+            <scope>provided</scope>
+        </dependency>
+
+</dependencies>
+      
     <build>
         <plugins>
             <plugin>
                 </configuration>
             </plugin>
         </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+            <resource>
+                <directory>src/external</directory>
+                <includes>
+                    <include>**/aws.json</include>
+                </includes>
+            </resource>
+        </resources>
     </build>
 </project>

File src/main/java/net/utoolity/bamboo/plugins/CloudFormationTask.java

 package net.utoolity.bamboo.plugins;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.Vector;
 
+import net.utoolity.bamboo.plugins.aws.CloudFormation;
+
 import org.apache.commons.lang.StringUtils;
 import org.jetbrains.annotations.NotNull;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.ISODateTimeFormat;
 
 import com.amazonaws.AmazonClientException;
 import com.amazonaws.AmazonServiceException;
 import com.amazonaws.services.cloudformation.AmazonCloudFormationClient;
 import com.amazonaws.services.cloudformation.model.CreateStackRequest;
 import com.amazonaws.services.cloudformation.model.DeleteStackRequest;
+import com.amazonaws.services.cloudformation.model.DescribeStackEventsRequest;
+import com.amazonaws.services.cloudformation.model.DescribeStackEventsResult;
 import com.amazonaws.services.cloudformation.model.DescribeStacksRequest;
 import com.amazonaws.services.cloudformation.model.Output;
 import com.amazonaws.services.cloudformation.model.Parameter;
 import com.amazonaws.services.cloudformation.model.Stack;
+import com.amazonaws.services.cloudformation.model.StackEvent;
 import com.amazonaws.services.cloudformation.model.StackStatus;
 import com.atlassian.bamboo.build.logger.BuildLogger;
 import com.atlassian.bamboo.configuration.ConfigurationMap;
 import com.atlassian.bamboo.task.TaskResultBuilder;
 import com.atlassian.bamboo.task.TaskType;
 import com.atlassian.bamboo.variable.CustomVariableContextImpl;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 
 public class CloudFormationTask extends CustomVariableContextImpl implements TaskType
 {
     private static final int WAIT_FOR_TRANSITION_INTERVAL = 5000;
-
-    // TODO: bootstrap this via an API, i.e. either via publicly available
-    // resources (IIRC boto offered these somewhere?!) or respectively
-    // restricted private IAM user.
-    private static final Map<String, String> ENDPOINT_MAP = ImmutableMap.<String, String> builder()
-            .put("eu-west-1", "cloudformation.eu-west-1.amazonaws.com")
-            .put("us-east-1", "cloudformation.us-east-1.amazonaws.com")
-            .put("ap-northeast-1", "cloudformation.ap-northeast-1.amazonaws.com")
-            .put("us-west-2", "cloudformation.us-west-2.amazonaws.com")
-            .put("us-west-1", "cloudformation.us-west-1.amazonaws.com")
-            .put("ap-southeast-1", "cloudformation.ap-southeast-1.amazonaws.com").build();
-
     private static final String STACK_STATUS_NO_SUCH_STACK = "NO_SUCH_STACK";
     private static final String STACK_REASON_NO_SUCH_STACK = "Stack has been deleted";
     @SuppressWarnings("unused")
             .addAll(STACK_STATUS_FAILED_SET)
             .add(StackStatus.ROLLBACK_COMPLETE.toString(), StackStatus.UPDATE_ROLLBACK_COMPLETE.toString()).build();
     private static final String CONTACT_SUPPORT = "Encountered internal plugin error - please contact support!";
-    
 
     @NotNull
     @java.lang.Override
         final String stackName = substituteString(configurationMap.get(CloudFormationTaskConfigurator.STACK_NAME));
         final String accessKey = configurationMap.get(CloudFormationTaskConfigurator.ACCESS_KEY);
         final String secretKey = configurationMap.get(CloudFormationTaskConfigurator.SECRET_KEY);
-
-        final String endpoint = ENDPOINT_MAP.get(stackRegion);
-        AmazonCloudFormation stackbuilder = new AmazonCloudFormationClient(
-                new BasicAWSCredentials(accessKey, secretKey));
-        buildLogger.addBuildLogEntry("Selected endpoint is " + endpoint + "(" + stackRegion + ")");
-        stackbuilder.setEndpoint(endpoint);
+        final String endpoint = CloudFormation.ENDPOINT_MAP.get(stackRegion);
 
         try
         {
+            AmazonCloudFormation cloudFormation = new AmazonCloudFormationClient(new BasicAWSCredentials(accessKey,
+                    secretKey));
+            buildLogger.addBuildLogEntry("Selecting endpoint is " + endpoint + " (" + stackRegion + ")");
+            cloudFormation.setEndpoint(endpoint);
+
             if (CloudFormationTaskConfigurator.CREATE_ACTION.equals(stackAction))
             {
                 final String templateSource = configurationMap.get(CloudFormationTaskConfigurator.TEMPLATE_SOURCE);
                     buildLogger.addBuildLogEntry("Selected template URL is " + templateURL);
                     final String transitionResult = createStack(stackName, templateSource, templateURL,
                             templateParameters, notificationARNs, creationTimeout, enableRollback, enableIAM,
-                            stackbuilder, buildLogger);
-                    determineTaskResult(stackName, transitionResult, stackbuilder, taskResultBuilder, buildLogger);
+                            cloudFormation, buildLogger);
+                    determineTaskResult(transitionResult, taskResultBuilder, buildLogger);
                 }
                 else if (CloudFormationTaskConfigurator.TEMPLATE_SOURCE_BODY.equals(templateSource))
                 {
                             .get(CloudFormationTaskConfigurator.TEMPLATE_BODY));
                     final String transitionResult = createStack(stackName, templateSource, templateBody,
                             templateParameters, notificationARNs, creationTimeout, enableRollback, enableIAM,
-                            stackbuilder, buildLogger);
-                    determineTaskResult(stackName, transitionResult, stackbuilder, taskResultBuilder, buildLogger);
+                            cloudFormation, buildLogger);
+                    determineTaskResult(transitionResult, taskResultBuilder, buildLogger);
                 }
                 else
                 {
             }
             else if (CloudFormationTaskConfigurator.DELETE_ACTION.equals(stackAction))
             {
-                final String transitionResult = deleteStack(stackName, stackbuilder, buildLogger);
-                determineTaskResult(stackName, transitionResult, stackbuilder, taskResultBuilder, buildLogger);
+                final String transitionResult = deleteStack(stackName, cloudFormation, buildLogger);
+                determineTaskResult(transitionResult, taskResultBuilder, buildLogger);
             }
             else
             {
      * @param notificationARNs
      * @param creationTimeout
      * @param enableRollback
-     * @param stackbuilder
+     * @param cloudFormation
      * @param buildLogger
      * @return String
      * @throws AmazonServiceException
     private String createStack(final String stackName, final String templateSourceType,
             final String templateSourceValue, final String templateParameters,
             final Collection<String> notificationARNs, final Integer timeoutInMinutes, final Boolean enableRollback,
-            final Boolean enableIAM, AmazonCloudFormation stackbuilder, final BuildLogger buildLogger)
+            final Boolean enableIAM, AmazonCloudFormation cloudFormation, final BuildLogger buildLogger)
             throws AmazonServiceException, AmazonClientException, Exception
     {
         CreateStackRequest createRequest = new CreateStackRequest().withStackName(stackName)
             createRequest.withCapabilities("CAPABILITY_IAM");
         }
         buildLogger.addBuildLogEntry("Creating stack '" + createRequest.getStackName() + "':");
-        stackbuilder.createStack(createRequest);
+        cloudFormation.createStack(createRequest);
 
         // Wait for the stack to be created
-        return waitForTransitionCompletion(stackbuilder, stackName, buildLogger);
+        return waitForTransitionCompletion(cloudFormation, stackName, buildLogger);
     }
 
     /**
      * @param stackName
      * @param templateBody
-     * @param stackbuilder
+     * @param cloudFormation
      * @param buildLogger
      * @return String
      * @throws AmazonServiceException
      * @throws AmazonClientException
      * @throws Exception
      */
-    private String deleteStack(final String stackName, AmazonCloudFormation stackbuilder, final BuildLogger buildLogger)
-            throws AmazonServiceException, AmazonClientException, Exception
+    private String deleteStack(final String stackName, AmazonCloudFormation cloudFormation,
+            final BuildLogger buildLogger) throws AmazonServiceException, AmazonClientException, Exception
     {
         // Delete the stack
         DeleteStackRequest deleteRequest = new DeleteStackRequest();
         deleteRequest.setStackName(stackName);
         buildLogger.addBuildLogEntry("Deleting stack '" + deleteRequest.getStackName() + "':");
-        stackbuilder.deleteStack(deleteRequest);
+        cloudFormation.deleteStack(deleteRequest);
 
         // Wait for the stack to be deleted
-        return waitForTransitionCompletion(stackbuilder, stackName, buildLogger);
+        return waitForTransitionCompletion(cloudFormation, stackName, buildLogger);
     }
 
     /**
-     * @param stackName
      * @param transitionResult
      * @param taskResultBuilder
      * @param buildLogger
      */
-    private void determineTaskResult(final String stackName, final String transitionResult,
-            AmazonCloudFormation stackbuilder, final TaskResultBuilder taskResultBuilder, final BuildLogger buildLogger)
+    private void determineTaskResult(final String transitionResult, final TaskResultBuilder taskResultBuilder,
+            final BuildLogger buildLogger)
     {
         if (BAMBOO_SUCCESS_SET.contains(transitionResult))
         {
      * Wait for a stack to complete transitioning (i.e. status not being in STACK_STATUS_IN_PROGRESS_SET or the stack no
      * longer existing).
      * 
-     * @param stackbuilder
+     * @param cloudFormation
      * @param stackName
      * @param BuildLogger
      * @throws Exception
      */
-    public String waitForTransitionCompletion(AmazonCloudFormation stackbuilder, String stackName,
+    public String waitForTransitionCompletion(AmazonCloudFormation cloudFormation, String stackName,
             BuildLogger buildLogger) throws Exception
     {
-        buildLogger.addBuildLogEntry("Waiting for transition ...");
-
         DescribeStacksRequest describeRequest = new DescribeStacksRequest();
         describeRequest.setStackName(stackName);
         Boolean transitionCompleted = false;
         String stackStatus = "Unknown";
         String stackReason = "Unspecified";
         Stack transitionedStack = null;
+        String nextToken = null;
+        Set<String> processedEventIds = new HashSet<String>();
 
         while (!transitionCompleted)
         {
             // deleted rather than returning an empty list.
             try
             {
-                List<Stack> stacks = stackbuilder.describeStacks(describeRequest).getStacks();
+                List<Stack> stacks = cloudFormation.describeStacks(describeRequest).getStacks();
                 if (stacks.isEmpty())
                 {
                     transitionCompleted = true;
                 {
                     for (Stack stack : stacks)
                     {
+                        DescribeStackEventsRequest stackEventsRequest = new DescribeStackEventsRequest().withStackName(
+                                stackName).withNextToken(nextToken);
+                        DescribeStackEventsResult stackEventsResult = cloudFormation
+                                .describeStackEvents(stackEventsRequest);
+                        nextToken = stackEventsResult.getNextToken();
+                        List<StackEvent> stackEventsView = Lists.reverse(stackEventsResult.getStackEvents());
+                        for (StackEvent stackEvent : stackEventsView)
+                        {
+                            // REVIEW: currently all states the stack has been in since creation are reported for
+                            // update and delete requests as well, which might be skipped eventually to reduce log
+                            // noise? [e.g. transitionStarted.isBefore(stackEvent.getTimestamp().getTime()]
+
+                            // Has this event already been processed once?
+                            if (processedEventIds.add(stackEvent.getEventId()))
+                            { // no
+                                buildLogger.addBuildLogEntry("... '"
+                                        + stackEvent.getLogicalResourceId()
+                                        + "' entered status "
+                                        + stackEvent.getResourceStatus()
+                                        + " ("
+                                        + new DateTime(stackEvent.getTimestamp()).toString(ISODateTimeFormat
+                                                .basicDateTimeNoMillis().withZone(DateTimeZone.UTC)) + ") ...");
+                            }
+                        }
                         if (!STACK_STATUS_IN_PROGRESS_SET.contains(stack.getStackStatus()))
                         {
                             transitionCompleted = true;
                 }
             }
 
-            // Sleep for 8 seconds until transition has completed.
+            // Sleep for WAIT_FOR_TRANSITION_INTERVAL seconds until transition has completed.
             if (!transitionCompleted)
             {
-                // Display some indication of progress.
-                buildLogger.addBuildLogEntry(".");
                 Thread.sleep(WAIT_FOR_TRANSITION_INTERVAL);
             }
         }
         {
             stackStatus = STACK_STATUS_NO_SUCH_STACK;
             stackReason = STACK_REASON_NO_SUCH_STACK;
-            buildLogger.addBuildLogEntry("... done: Transition of stack '" + stackName + "' completed with status "
-                    + stackStatus + " (" + stackReason + ").");
+            buildLogger.addBuildLogEntry("Transition of stack '" + stackName + "' completed with status " + stackStatus
+                    + " (" + stackReason + ").");
         }
         else
         {
             stackStatus = transitionedStack.getStackStatus();
             stackReason = transitionedStack.getStackStatusReason();
-            buildLogger.addBuildLogEntry("... done: Transition of stack '" + stackName + "' completed with status "
-                    + stackStatus + " (" + stackReason + ").");
+            buildLogger.addBuildLogEntry("Transition of stack '" + stackName + "' completed with status " + stackStatus
+                    + " (" + stackReason + ").");
             describeOutputs(transitionedStack, buildLogger);
         }
 
 
         return result;
     }
+
 }

File src/main/java/net/utoolity/bamboo/plugins/CloudFormationTaskConfigurator.java

 package net.utoolity.bamboo.plugins;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Map;
 import java.util.Set;
 
+import net.utoolity.bamboo.plugins.aws.CloudFormation;
+
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import com.atlassian.bamboo.task.AbstractTaskConfigurator;
 import com.atlassian.bamboo.task.TaskDefinition;
 import com.atlassian.bamboo.utils.error.ErrorCollection;
-
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.opensymphony.xwork.TextProvider;
     public static final String DEFAULT_ROLLBACK = OPTION_TRUE;
     public static final String DEFAULT_IAM = OPTION_FALSE;
 
-    // TODO: bootstrap this via API, i.e. either via publicly available
-    // resources (IIRC boto offered these somewhere?!) or respectively
-    // restricted private IAM user.
-    private static final Map<String, String> REGION_MAP = ImmutableMap.<String, String> builder()
-            .put("eu-west-1", "eu-west-1").put("us-east-1", "us-east-1").put("ap-northeast-1", "ap-northeast-1")
-            .put("us-west-2", "us-west-2").put("us-west-1", "us-west-1").put("ap-southeast-1", "ap-southeast-1")
-            .build();
     public static final String DEFAULT_REGION = "us-east-1";
 
     public static final String STACK_NAME_SAMPLE = "CloudFormationSampleStack";
     public void populateContextForCreate(@NotNull final Map<String, Object> context)
     {
         super.populateContextForCreate(context);
+
         context.put(STACK_ACTIONS, ACTION_MAP);
         context.put(STACK_ACTION, DEFAULT_ACTION);
-        context.put(STACK_REGIONS, REGION_MAP);
+        context.put(STACK_REGIONS, CloudFormation.REGION_MAP);
         context.put(STACK_REGION, DEFAULT_REGION);
         context.put(STACK_NAME, STACK_NAME_SAMPLE);
         context.put(TEMPLATE_SOURCES, SOURCE_MAP);
         context.put(TEMPLATE_PARAMETERS, DEFAULT_TEMPLATE_PARAMETERS);
         try
         {
-            final String templateBody = convertStreamToString(CloudFormationTaskConfigurator.class
-                    .getResourceAsStream("CloudFormationSample.template"));
+            final String templateBody = IOUtils.toString(CloudFormationTaskConfigurator.class
+                    .getResourceAsStream("CloudFormationSample.template"), "UTF-8");
             context.put(TEMPLATE_BODY, templateBody);
         }
         catch (IOException ioe)
         super.populateContextForEdit(context, taskDefinition);
         taskConfiguratorHelper.populateContextWithConfiguration(context, taskDefinition, FIELD_SET);
         context.put(STACK_ACTIONS, ACTION_MAP);
-        context.put(STACK_REGIONS, REGION_MAP);
+        context.put(STACK_REGIONS, CloudFormation.REGION_MAP);
         context.put(TEMPLATE_SOURCES, SOURCE_MAP);
         context.put(ROLLBACK_OPTIONS, ROLLBACK_MAP);
         context.put("mode", "edit");
         this.textProvider = textProvider;
     }
 
-    private String convertStreamToString(InputStream resourceAsStream) throws IOException
-    {
-        // Convert a stream into a single, newline separated string
-        BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream));
-        StringBuilder stringbuilder = new StringBuilder();
-        String line = null;
-        while ((line = reader.readLine()) != null)
-        {
-            stringbuilder.append(line + "\n");
-        }
-        resourceAsStream.close();
-        return stringbuilder.toString();
-    }
-
     public static Integer tryParsePositiveInteger(String text)
     {
         try

File src/main/java/net/utoolity/bamboo/plugins/aws/AWS.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.aws;
+
+import java.io.IOException;
+import java.net.URI;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriBuilder;
+
+import net.utoolity.bamboo.plugins.missingcloud.Service;
+import net.utoolity.bamboo.plugins.missingcloud.ServiceRegistry;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.DeserializationConfig;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.mrbean.MrBeanModule;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter;
+import com.sun.jersey.client.apache4.ApacheHttpClient4;
+
+public final class AWS
+{
+    private static final Logger LOG = LoggerFactory.getLogger(AWS.class);
+
+    private static final String SERVICE_REGISTRY_BASE_URI = "https://raw.github.com/garnaat/missingcloud/master";
+    private static final String SERVICE_REGISTRY_DIRECTORY = "/com/github/garnaat/missingcloud";
+    private static final String SERVICE_REGISTRY_RESOURCE = "aws.json";
+    private static final int JERSEY_CLIENT_DEFAULT_TIMEOUT = 8192;
+    // REVIEW: At first sight this should be a static final, OTOH Bamboo is a potentially long running
+    // application, which triggers the need to invalidate this 'cache' once in a while; for the time being.
+    private static final ServiceRegistry SERVICE_REGISTRY = mapServiceRegistry();
+
+    /**
+     * @return
+     */
+    private static ServiceRegistry mapServiceRegistry()
+    {
+        ServiceRegistry serviceRegistry = null;
+        try
+        {
+            // REVIEW: this is taking the MrBean shortcut to avoid explicit mapping of all JSON properties, however,
+            // given these will rarely change, it would probably make sense to reduce complexity and dependencies by
+            // doing the explicit mapping indeed (this even more so given jackson-core is provided by Bamboo).
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.registerModule(new MrBeanModule());
+            mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+            // KISS: Try remote service registry resource first and fall back to local in case.
+            // REVIEW: Better error handling might still help ...
+            String content = null;
+            LOG.info("Mapping service registry: ...");
+            try
+            {
+                content = getRemoteServiceRegistry();
+                LOG.info("... fetched remote resource.");
+            }
+            catch (UniformInterfaceException e)
+            {
+                e.getResponse().toString();
+            }
+            if (StringUtils.isEmpty(content))
+            {
+                LOG.info("... fetched local resource.");
+                content = getLocalServiceRegistry();
+            }
+            serviceRegistry = mapper.readValue(content, ServiceRegistry.class);
+        }
+        catch (JsonParseException e)
+        {
+            LOG.error("Unable to parse JSON of service registry resource.", e);
+        }
+        catch (JsonMappingException e)
+        {
+            LOG.error("Unable to map JSON of service registry resource.", e);
+        }
+        catch (IOException e)
+        {
+            LOG.error("Unable to load local service registry resource.", e);
+        }
+
+        return serviceRegistry;
+    }
+
+    /**
+     * @return
+     */
+    private static URI getServiceRegistryBaseURI()
+    {
+        return UriBuilder.fromUri(SERVICE_REGISTRY_BASE_URI).build();
+    }
+
+    /**
+     * @return
+     * @throws IOException
+     */
+    private static String getLocalServiceRegistry() throws IOException
+    {
+        final String content = IOUtils.toString(
+                AWS.class.getResourceAsStream(SERVICE_REGISTRY_DIRECTORY + "/" + SERVICE_REGISTRY_RESOURCE), "UTF-8");
+        return content;
+    }
+
+    /**
+     * @return
+     * @throws UniformInterfaceException
+     */
+    private static String getRemoteServiceRegistry() throws UniformInterfaceException
+    {
+        Client client = ApacheHttpClient4.create();
+        client.setConnectTimeout(JERSEY_CLIENT_DEFAULT_TIMEOUT);
+        client.setFollowRedirects(true);
+        // NOTE: Contrary to http://jersey.java.net/nonav/documentation/latest/user-guide.html#d4e700 adding the
+        // LoggingFilter seems to be at odds with GZIP encoding, which leaks to the LOG, so should be enabled on an
+        // either/or base for debugging only.
+        // client.addFilter(new LoggingFilter());
+        client.addFilter(new GZIPContentEncodingFilter());
+        WebResource webResource = client.resource(getServiceRegistryBaseURI());
+        final String content = webResource.path(SERVICE_REGISTRY_RESOURCE).accept(MediaType.APPLICATION_JSON_TYPE)
+                .get(String.class);
+        return content;
+    }
+
+    public static Service getService(String name)
+    {
+        return SERVICE_REGISTRY.getServices().get(name);
+    }
+}

File src/main/java/net/utoolity/bamboo/plugins/aws/CloudFormation.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.aws;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+
+import net.utoolity.bamboo.plugins.missingcloud.Region;
+import net.utoolity.bamboo.plugins.missingcloud.Service;
+
+public final class CloudFormation
+{
+    public static final Map<String, String> REGION_MAP = mapRegions();
+    public static final Map<String, String> ENDPOINT_MAP = mapEndpoints();
+
+    private static Map<String, String> mapRegions()
+    {
+        Map<String, String> regions = new HashMap<String, String>();
+        Service cloudFormation = AWS.getService("CloudFormation");
+        for (Region region : cloudFormation.getRegions())
+        {
+            regions.put(region.getName(), region.getDescription());
+        }
+
+        return ImmutableMap.<String, String> builder().putAll(regions).build();
+    }
+
+    private static Map<String, String> mapEndpoints()
+    {
+        Map<String, String> endpoints = new HashMap<String, String>();
+        Service cloudFormation = AWS.getService("CloudFormation");
+        for (Region region : cloudFormation.getRegions())
+        {
+            endpoints.put(region.getName(), region.getEndpoint());
+        }
+        
+        return ImmutableMap.<String, String> builder().putAll(endpoints).build();
+    }
+}

File src/main/java/net/utoolity/bamboo/plugins/missingcloud/Region.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.missingcloud;
+
+
+public interface Region
+{
+    String getEndpoint();
+
+    String getName();
+
+    String getDescription();
+}

File src/main/java/net/utoolity/bamboo/plugins/missingcloud/Service.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.missingcloud;
+
+import java.util.List;
+
+import org.codehaus.jackson.annotate.JsonProperty;
+
+public interface Service
+{
+    String getName();
+
+    String getDescription();
+
+    String getAuthentication();
+
+    @JsonProperty("api_version")
+    String getApiVersion();
+
+    String getStatus();
+
+    List<Region> getRegions();
+
+    String getPath();
+
+    String getPort();
+}

File src/main/java/net/utoolity/bamboo/plugins/missingcloud/ServiceRegistry.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.missingcloud;
+
+import java.util.Map;
+
+
+public interface ServiceRegistry
+{
+    String getName();
+
+    String getDescription();
+
+    /**
+     * @return all services
+     */
+    Map<String, Service> getServices();
+}

File src/main/resources/atlassian-plugin.xml

-<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
-    <plugin-info>
-        <description>${project.description}</description>
-        <version>${project.version}</version>
-        <vendor name="${project.organization.name}" url="${project.organization.url}" />
-    </plugin-info>
-
-    <resource type="i18n" name="i18n" location="${groupId}/i18n"/>
-
-    <taskType name="AWS CloudFormation" class="net.utoolity.bamboo.plugins.CloudFormationTask" key="cft">
-      <description>Create/Delete a CloudFormation stack.</description>
-      <category name="builder"/>
-      <category name="deployment"/>
-      <category name="test"/>
-      <configuration class="net.utoolity.bamboo.plugins.CloudFormationTaskConfigurator"/>
-      <resource type="freemarker" name="edit" location="${atlassian.plugin.directory}/editCloudFormationTask.ftl"/>
-      <resource type="freemarker" name="view" location="${atlassian.plugin.directory}/viewCloudFormationTask.ftl"/>
-    </taskType>
-
-    <taskType name="Amazon Elastic Compute Cloud (EC2)" class="net.utoolity.bamboo.plugins.EC2Task" key="ec2t">
-      <description>Start/Stop/Reboot an EC2 instance.</description>
-      <category name="deployment"/>
-      <category name="test"/>
-      <configuration class="net.utoolity.bamboo.plugins.EC2TaskConfigurator"/>
-      <resource type="freemarker" name="edit" location="${atlassian.plugin.directory}/editEC2Task.ftl"/>
-      <resource type="freemarker" name="view" location="${atlassian.plugin.directory}/viewEC2Task.ftl"/>
-    </taskType>
-</atlassian-plugin>
+<atlassian-plugin key="${project.groupId}.${project.artifactId}"
+                  name="${project.name}"
+                  plugins-version="2">
+  <plugin-info>
+    <description>${project.description}</description>
+    <version>${project.version}</version>
+    <vendor name="${project.organization.name}"
+            url="${project.organization.url}" />
+  </plugin-info>
+  <resource type="i18n"
+            name="i18n"
+            location="${groupId}/i18n" />
+  <taskType name="AWS CloudFormation"
+            class="net.utoolity.bamboo.plugins.CloudFormationTask"
+            key="cft">
+    <description>Create/Delete a CloudFormation
+    stack.</description>
+    <category name="builder" />
+    <category name="deployment" />
+    <category name="test" />
+    <configuration class="net.utoolity.bamboo.plugins.CloudFormationTaskConfigurator" />
+    <resource type="download"
+              name="icon"
+              location="${atlassian.plugin.directory}/CloudFormationTaskIcon.png" />
+    <resource type="freemarker"
+              name="edit"
+              location="${atlassian.plugin.directory}/editCloudFormationTask.ftl" />
+    <resource type="freemarker"
+              name="view"
+              location="${atlassian.plugin.directory}/viewCloudFormationTask.ftl" />
+  </taskType>
+  <taskType name="Amazon Elastic Compute Cloud (EC2)"
+            class="net.utoolity.bamboo.plugins.EC2Task"
+            key="ec2t">
+    <description>Start/Stop/Reboot an EC2 instance.</description>
+    <category name="deployment" />
+    <category name="test" />
+    <configuration class="net.utoolity.bamboo.plugins.EC2TaskConfigurator" />
+    <resource type="freemarker"
+              name="edit"
+              location="${atlassian.plugin.directory}/editEC2Task.ftl" />
+    <resource type="freemarker"
+              name="view"
+              location="${atlassian.plugin.directory}/viewEC2Task.ftl" />
+  </taskType>
+</atlassian-plugin>

File src/main/resources/net/utoolity/bamboo/plugins/CloudFormationTaskIcon.png

Added
New image

File src/main/resources/net/utoolity/bamboo/plugins/i18n.properties

 net.utoolity.bamboo.plugins.creationTimeout.description = (Optional) How much time can pass before the stack creation is considered failed?
 net.utoolity.bamboo.plugins.creationTimeout.error = You did not specify a positive integer value.
 net.utoolity.bamboo.plugins.enableRollback = Rollback on failure?
-net.utoolity.bamboo.plugins.enableRollback.description = Should the stack creation be rolled back on failure? 
+net.utoolity.bamboo.plugins.enableRollback.description = Should the stack creation be rolled back on failure?
 net.utoolity.bamboo.plugins.enableIAM = Enable IAM?
-net.utoolity.bamboo.plugins.enableIAM.description = Should the stack be allowed to access IAM resources? 
+net.utoolity.bamboo.plugins.enableIAM.description = Should the stack be allowed to access IAM resources?
 net.utoolity.bamboo.plugins.enableIAM.error = You did not enable access to IAM, but the stack requires it.
 net.utoolity.bamboo.plugins.instanceAction = Instance Action
 net.utoolity.bamboo.plugins.instanceAction.description = Which instance action do you request?

File src/test/java/net/utoolity/bamboo/plugins/aws/AWSTestCase.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.aws;
+
+import junit.framework.TestCase;
+import net.utoolity.bamboo.plugins.missingcloud.Service;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class AWSTestCase extends TestCase
+{
+    /*
+     * (non-Javadoc)
+     * 
+     * @see junit.framework.TestCase#setUp()
+     */
+    @Before
+    protected void setUp() throws Exception
+    {
+        super.setUp();
+    }
+
+    /**
+     * Test method for {@link net.utoolity.bamboo.plugins.aws.AWS#getService(java.lang.String)}.
+     */
+    @Test
+    public void testGetService()
+    {
+        final Service service = AWS.getService("CloudFormation");
+        final String actual = service.getName();
+        assertEquals("cfn", actual);
+    }
+}

File src/test/java/net/utoolity/bamboo/plugins/aws/CloudFormationTestCase.java

+/**
+ * 
+ */
+package net.utoolity.bamboo.plugins.aws;
+
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class CloudFormationTestCase extends TestCase
+{
+    private static final int NUM_KNOWN_REGIONS = 6;
+    private Map<String, String> regionMap;
+    private Map<String, String> endpointMap;
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see junit.framework.TestCase#setUp()
+     */
+    @Before
+    protected void setUp() throws Exception
+    {
+        super.setUp();
+        regionMap = CloudFormation.REGION_MAP;
+        endpointMap = CloudFormation.ENDPOINT_MAP;
+    }
+
+    @Test
+    public void testRegionMap()
+    {
+        assertTrue(NUM_KNOWN_REGIONS <= regionMap.size());
+        assertEquals("US-East (Northern Virginia)", regionMap.get("us-east-1"));
+    }
+
+    @Test
+    public void testEndpointMap()
+    {
+        assertTrue(NUM_KNOWN_REGIONS <= endpointMap.size());
+        assertEquals("cloudformation.us-east-1.amazonaws.com", endpointMap.get("us-east-1"));
+    }
+
+}