Commits

Steffen Opel committed e7c4e38

BAWS-69/BAWS-70/BAWS-71/BAWS-72: Added initial task to operate an EC2 instance.

  • Participants
  • Parent commits 4eabcfa
  • Branches BAWS-68

Comments (0)

Files changed (6)

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

+package net.utoolity.bamboo.plugins;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.amazonaws.AmazonClientException;
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.ec2.AmazonEC2;
+import com.amazonaws.services.ec2.AmazonEC2Client;
+import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
+import com.amazonaws.services.ec2.model.DescribeInstancesResult;
+import com.amazonaws.services.ec2.model.Instance;
+import com.amazonaws.services.ec2.model.InstanceStateChange;
+import com.amazonaws.services.ec2.model.RebootInstancesRequest;
+import com.amazonaws.services.ec2.model.Reservation;
+import com.amazonaws.services.ec2.model.StartInstancesRequest;
+import com.amazonaws.services.ec2.model.StartInstancesResult;
+import com.amazonaws.services.ec2.model.StopInstancesRequest;
+import com.amazonaws.services.ec2.model.StopInstancesResult;
+import com.atlassian.bamboo.build.logger.BuildLogger;
+import com.atlassian.bamboo.configuration.ConfigurationMap;
+import com.atlassian.bamboo.task.TaskContext;
+import com.atlassian.bamboo.task.TaskException;
+import com.atlassian.bamboo.task.TaskResult;
+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;
+
+public class EC2Task extends CustomVariableContextImpl implements TaskType
+{
+    private static final int WAIT_FOR_TRANSITION_INTERVAL = 2000;
+
+    // TODO: bootstrap this via an API, i.e. either via publicly available resources
+    // (https://github.com/garnaat/missingcloud) or respectively restricted private IAM user.
+    private static final Map<String, String> ENDPOINT_MAP = ImmutableMap.<String, String> builder()
+            .put("eu-west-1", "ec2.eu-west-1.amazonaws.com").put("us-east-1", "ec2.us-east-1.amazonaws.com")
+            .put("ap-northeast-1", "ec2.ap-northeast-1.amazonaws.com").put("us-west-2", "ec2.us-west-2.amazonaws.com")
+            .put("us-west-1", "ec2.us-west-1.amazonaws.com").put("ap-southeast-1", "ec2.ap-southeast-1.amazonaws.com")
+            .build();
+
+    // REVIEW: There should be a respective enum defined somewhere?!
+    private static final Set<String> INSTANCE_STATE_IN_PROGRESS_SET = ImmutableSet.<String> builder()
+            .add("pending", "shutting-down", "stopping").build();
+    private static final Set<String> INSTANCE_STATE_COMPLETE_SET = ImmutableSet.<String> builder()
+            .add("running", "terminated", "stopped").build();
+    private static final Set<String> BAMBOO_SUCCESS_SET = ImmutableSet.<String> builder()
+            .addAll(INSTANCE_STATE_COMPLETE_SET).build();
+    private static final Set<String> BAMBOO_FAILED_SET = ImmutableSet.<String> builder()
+            .addAll(INSTANCE_STATE_IN_PROGRESS_SET).build();
+    private static final String CONTACT_SUPPORT = "Encountered internal plugin error - please contact support!";
+
+    @NotNull
+    @java.lang.Override
+    public TaskResult execute(@NotNull final TaskContext taskContext) throws TaskException, AmazonServiceException,
+            AmazonClientException
+    {
+        final BuildLogger buildLogger = taskContext.getBuildLogger();
+        final TaskResultBuilder taskResultBuilder = TaskResultBuilder.create(taskContext);
+        final ConfigurationMap configurationMap = taskContext.getConfigurationMap();
+
+        final String instanceAction = configurationMap.get(EC2TaskConfigurator.INSTANCE_ACTION);
+        final String instanceRegion = configurationMap.get(EC2TaskConfigurator.INSTANCE_REGION);
+        final String instanceId = substituteString(configurationMap.get(EC2TaskConfigurator.INSTANCE_ID));
+        final Boolean forceStop = configurationMap.getAsBoolean(EC2TaskConfigurator.FORCE_STOP);
+        final String accessKey = configurationMap.get(EC2TaskConfigurator.ACCESS_KEY);
+        final String secretKey = configurationMap.get(EC2TaskConfigurator.SECRET_KEY);
+
+        final String endpoint = ENDPOINT_MAP.get(instanceRegion);
+        AmazonEC2 ec2 = new AmazonEC2Client(new BasicAWSCredentials(accessKey, secretKey));
+        buildLogger.addBuildLogEntry("Selecting endpoint " + endpoint + " (" + instanceRegion + ")");
+        ec2.setEndpoint(endpoint);
+
+        try
+        {
+            if (EC2TaskConfigurator.START_ACTION.equals(instanceAction))
+            {
+                final String transitionResult = startInstance(instanceId, ec2, buildLogger);
+                determineTaskResult(transitionResult, taskResultBuilder, buildLogger);
+            }
+            else if (EC2TaskConfigurator.STOP_ACTION.equals(instanceAction))
+            {
+                final String transitionResult = stopInstance(instanceId, forceStop, ec2, buildLogger);
+                determineTaskResult(transitionResult, taskResultBuilder, buildLogger);
+            }
+            else if (EC2TaskConfigurator.REBOOT_ACTION.equals(instanceAction))
+            {
+                // REVIEW: is rebootInstance() really a non traceable and never failing operation like so or is there
+                // an option to check for success by some indirect means regardless?
+                rebootInstance(instanceId, ec2, buildLogger);
+                taskResultBuilder.success();
+            }
+            else
+            {
+                buildLogger.addErrorLogEntry(CONTACT_SUPPORT);
+                taskResultBuilder.failedWithError();
+            }
+        }
+        catch (AmazonServiceException ase)
+        {
+            buildLogger.addErrorLogEntry("Instance request rejected by AWS!", ase);
+            taskResultBuilder.failedWithError();
+
+        }
+        catch (AmazonClientException ace)
+        {
+            buildLogger.addErrorLogEntry("Failed to communicate with AWS!", ace);
+            taskResultBuilder.failedWithError();
+        }
+        catch (Exception e)
+        {
+            buildLogger.addErrorLogEntry("Failed to fetch resource from AWS!", e);
+            taskResultBuilder.failedWithError();
+        }
+
+        return taskResultBuilder.build();
+    }
+
+    /**
+     * @param instanceId
+     * @param templateBody
+     * @param notificationARNs
+     * @param creationTimeout
+     * @param enableRollback
+     * @param ec2
+     * @param buildLogger
+     * @return String
+     * @throws AmazonServiceException
+     * @throws AmazonClientException
+     * @throws InterruptedException
+     * @throws Exception
+     */
+    private String startInstance(final String instanceId, AmazonEC2 ec2, final BuildLogger buildLogger)
+            throws AmazonServiceException, AmazonClientException, InterruptedException
+    {
+        // Stop the instance
+        StartInstancesRequest startRequest = new StartInstancesRequest().withInstanceIds(instanceId);
+        StartInstancesResult startResult = ec2.startInstances(startRequest);
+        List<InstanceStateChange> stateChangeList = startResult.getStartingInstances();
+
+        // Wait for the instance to be stopped
+        return waitForTransitionCompletion(stateChangeList, "running", ec2, instanceId, buildLogger);
+    }
+
+    /**
+     * @param instanceId
+     * @param doForce
+     * @param ec2
+     * @param buildLogger
+     * @return String
+     * @throws AmazonServiceException
+     * @throws AmazonClientException
+     * @throws InterruptedException
+     * @throws Exception
+     */
+    private String stopInstance(final String instanceId, final Boolean forceStop, AmazonEC2 ec2,
+            final BuildLogger buildLogger) throws AmazonServiceException, AmazonClientException, InterruptedException
+    {
+        // Stop the instance
+        StopInstancesRequest stopRequest = new StopInstancesRequest().withInstanceIds(instanceId).withForce(forceStop);
+        StopInstancesResult startResult = ec2.stopInstances(stopRequest);
+        List<InstanceStateChange> stateChangeList = startResult.getStoppingInstances();
+
+        // Wait for the instance to be stopped
+        return waitForTransitionCompletion(stateChangeList, "stopped", ec2, instanceId, buildLogger);
+    }
+
+    /**
+     * @param instanceId
+     * @param ec2
+     * @param buildLogger
+     * @return String
+     * @throws AmazonServiceException
+     * @throws AmazonClientException
+     * @throws Exception
+     */
+    private void rebootInstance(final String instanceId, AmazonEC2 ec2, final BuildLogger buildLogger)
+            throws AmazonServiceException, AmazonClientException
+    {
+        // Reboot the instance
+        RebootInstancesRequest rebootRequest = new RebootInstancesRequest().withInstanceIds(instanceId);
+        buildLogger.addBuildLogEntry("Rebooting instance '" + rebootRequest.getInstanceIds() + "'");
+        ec2.rebootInstances(rebootRequest);
+    }
+
+    /**
+     * @param transitionResult
+     * @param taskResultBuilder
+     * @param buildLogger
+     */
+    private void determineTaskResult(final String transitionResult, final TaskResultBuilder taskResultBuilder,
+            final BuildLogger buildLogger)
+    {
+        if (BAMBOO_SUCCESS_SET.contains(transitionResult))
+        {
+            taskResultBuilder.success();
+        }
+        else if (BAMBOO_FAILED_SET.contains(transitionResult))
+        {
+            taskResultBuilder.failed();
+        }
+        else
+        {
+            buildLogger.addErrorLogEntry(CONTACT_SUPPORT);
+            taskResultBuilder.failedWithError();
+        }
+    }
+
+    /**
+     * Wait for a instance to complete transitioning (i.e. status not being in INSTANCE_STATE_IN_PROGRESS_SET or the
+     * instance no longer existing).
+     * 
+     * @param stateChangeList
+     * @param instancebuilder
+     * @param instanceId
+     * @param BuildLogger
+     * @throws InterruptedException
+     * @throws Exception
+     */
+    public final String waitForTransitionCompletion(List<InstanceStateChange> stateChangeList,
+            final String desiredState, AmazonEC2 instancebuilder, String instanceId, BuildLogger buildLogger)
+            throws InterruptedException
+    {
+        Boolean transitionCompleted = false;
+        InstanceStateChange stateChange = stateChangeList.get(0);
+        String previousState = stateChange.getPreviousState().getName();
+        String currentState = stateChange.getCurrentState().getName();
+        buildLogger.addBuildLogEntry("Transition of instance '" + instanceId + "' from previous state " + previousState
+                + " to desired state " + desiredState + " initiated");
+
+        while (!transitionCompleted)
+        {
+            try
+            {
+                Instance instance = describeInstance(instancebuilder, instanceId);
+                currentState = instance.getState().getName();
+                buildLogger.addBuildLogEntry("... previous state is " + previousState + ", current state is "
+                        + currentState + " ...");
+                previousState = currentState;
+
+                if (currentState.equals(desiredState))
+                {
+                    transitionCompleted = true;
+                }
+            }
+            catch (AmazonServiceException ase)
+            {
+                // KLUDGE: see REVIEW above for the reasoning behind this.
+                if (ase.getMessage().equals("Instance:" + instanceId + " does not exist"))
+                {
+                    transitionCompleted = true;
+                }
+                else
+                {
+                    buildLogger.addErrorLogEntry("Failed to describe instance!", ase);
+                    throw ase;
+                }
+            }
+
+            // Sleep for WAIT_FOR_TRANSITION_INTERVAL seconds until transition has completed.
+            if (!transitionCompleted)
+            {
+                Thread.sleep(WAIT_FOR_TRANSITION_INTERVAL);
+            }
+        }
+
+        buildLogger.addBuildLogEntry("Transitioning instance '" + instanceId + "' to state " + desiredState
+                + " completed with state " + currentState + ".");
+
+        return currentState;
+    }
+
+    /**
+     * @param instancebuilder
+     * @param instanceId
+     * @param describeRequest
+     * @return String
+     * @throws AmazonServiceException
+     * @throws AmazonClientException
+     */
+    public static Instance describeInstance(AmazonEC2 instancebuilder, String instanceId)
+            throws AmazonServiceException, AmazonClientException
+    {
+        DescribeInstancesRequest describeRequest = new DescribeInstancesRequest().withInstanceIds(instanceId);
+        DescribeInstancesResult result = instancebuilder.describeInstances(describeRequest);
+
+        for (Reservation reservation : result.getReservations())
+        {
+            for (Instance instance : reservation.getInstances())
+            {
+                if (instanceId.equals(instance.getInstanceId()))
+                {
+                    return instance;
+                }
+            }
+        }
+        return null;
+    }
+}

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

+package net.utoolity.bamboo.plugins;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import com.atlassian.bamboo.collections.ActionParametersMap;
+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 class EC2TaskConfigurator extends AbstractTaskConfigurator
+{
+    public static final String INSTANCE_ACTIONS = "instanceActions";
+    public static final String INSTANCE_ACTION = "instanceAction";
+    public static final String INSTANCE_REGIONS = "instanceRegions";
+    public static final String INSTANCE_REGION = "instanceRegion";
+    public static final String INSTANCE_ID = "instanceId";
+    public static final String FORCE_STOP = "forceStop";
+    public static final String ACCESS_KEY = "accessKey";
+    public static final String SECRET_KEY = "secretKey";
+    private static final Set<String> FIELD_SET = ImmutableSet.<String> builder()
+            .add(INSTANCE_ACTION, INSTANCE_REGION, INSTANCE_ID, FORCE_STOP, ACCESS_KEY, SECRET_KEY).build();
+    public static final String CHANGE_KEY = "changeKey";
+    public static final String NEW_KEY = "newKey";
+
+    public static final String START_ACTION = "Start";
+    public static final String STOP_ACTION = "Stop";
+    public static final String REBOOT_ACTION = "Reboot";
+    private static final Map<String, String> ACTION_MAP = ImmutableMap.<String, String> builder()
+            .put(START_ACTION, START_ACTION).put(STOP_ACTION, STOP_ACTION).put(REBOOT_ACTION, REBOOT_ACTION).build();
+    public static final String DEFAULT_ACTION = START_ACTION;
+    public static final String OPTION_TRUE = "True";
+    public static final String OPTION_FALSE = "False";
+    public static final String DEFAULT_FORCE_STOP = OPTION_FALSE;
+
+    // TODO: bootstrap this via an API, i.e. either via publicly available resources
+    // (https://github.com/garnaat/missingcloud) 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";
+
+    @SuppressWarnings("unused")
+    private TextProvider textProvider;
+
+    @NotNull
+    @Override
+    public Map<String, String> generateTaskConfigMap(@NotNull final ActionParametersMap params,
+            @Nullable final TaskDefinition previousTaskDefinition)
+    {
+        final Map<String, String> config = super.generateTaskConfigMap(params, previousTaskDefinition);
+        taskConfiguratorHelper.populateTaskConfigMapWithActionParameters(config, params, FIELD_SET);
+
+        String changeKey = params.getString(CHANGE_KEY);
+        if ("true".equals(changeKey))
+        {
+            final String newKey = params.getString(NEW_KEY);
+            config.put(SECRET_KEY, newKey);
+        }
+        else if (previousTaskDefinition != null)
+        {
+            config.put(SECRET_KEY, previousTaskDefinition.getConfiguration().get(SECRET_KEY));
+        }
+        else
+        {
+            final String secretKey = params.getString(SECRET_KEY);
+            config.put(SECRET_KEY, secretKey);
+        }
+
+        if (previousTaskDefinition != null)
+        {
+            config.put(INSTANCE_ACTION, previousTaskDefinition.getConfiguration().get(INSTANCE_ACTION));
+        }
+
+        return config;
+    }
+
+    @Override
+    public void populateContextForCreate(@NotNull final Map<String, Object> context)
+    {
+        super.populateContextForCreate(context);
+        context.put(INSTANCE_ACTIONS, ACTION_MAP);
+        context.put(INSTANCE_ACTION, DEFAULT_ACTION);
+        context.put(INSTANCE_REGIONS, REGION_MAP);
+        context.put(INSTANCE_REGION, DEFAULT_REGION);
+        context.put(FORCE_STOP, DEFAULT_FORCE_STOP);
+        context.put("mode", "create");
+    }
+
+    @Override
+    public void populateContextForEdit(@NotNull final Map<String, Object> context,
+            @NotNull final TaskDefinition taskDefinition)
+    {
+        super.populateContextForEdit(context, taskDefinition);
+        taskConfiguratorHelper.populateContextWithConfiguration(context, taskDefinition, FIELD_SET);
+        context.put(INSTANCE_ACTIONS, ACTION_MAP);
+        context.put(INSTANCE_REGIONS, REGION_MAP);
+        context.put("mode", "edit");
+    }
+
+    @Override
+    public void populateContextForView(@NotNull final Map<String, Object> context,
+            @NotNull final TaskDefinition taskDefinition)
+    {
+        super.populateContextForView(context, taskDefinition);
+        taskConfiguratorHelper.populateContextWithConfiguration(context, taskDefinition, FIELD_SET);
+    }
+
+    @Override
+    public void validate(@NotNull final ActionParametersMap params, @NotNull final ErrorCollection errorCollection)
+    {
+        super.validate(params, errorCollection);
+
+        // KLUDGE/REVIEW: the i18n text should be provided by
+        // textProvider.getText() apparently as per the code generated
+        // via 'atlas-create-bamboo-plugin', however, this does not work at all,
+        // see https://answers.atlassian.com/questions/20566 for a discussion;
+        // replacing the call with getI18nBean().getText() works fine though.
+        final String instanceIdValue = params.getString(INSTANCE_ID);
+        if (StringUtils.isEmpty(instanceIdValue))
+        {
+            errorCollection
+                    .addError(INSTANCE_ID, getI18nBean().getText("net.utoolity.bamboo.plugins.instanceId.error"));
+        }
+        else
+        {
+            // TODO: validate root-device-type (must not be 'instance-store' for start/stop actions)!
+            // TODO: validate instance-lifecycle (must not be 'spot' for start/stop actions)!
+        }
+        final String accessKeyValue = params.getString(ACCESS_KEY);
+        if (StringUtils.isEmpty(accessKeyValue))
+        {
+            errorCollection.addError(ACCESS_KEY, getI18nBean().getText("net.utoolity.bamboo.plugins.accessKey.error"));
+        }
+        final String secretKeyValue = params.getString(SECRET_KEY);
+        final String changeKey = params.getString(CHANGE_KEY);
+        final String newKeyValue = params.getString(NEW_KEY);
+        if ("true".equals(changeKey))
+        {
+            if (StringUtils.isEmpty(newKeyValue))
+            {
+                errorCollection.addError(NEW_KEY, getI18nBean().getText("net.utoolity.bamboo.plugins.newKey.error"));
+            }
+        }
+        else if (null == changeKey && null != secretKeyValue && secretKeyValue.isEmpty())
+        {
+            errorCollection.addError(SECRET_KEY, getI18nBean().getText("net.utoolity.bamboo.plugins.secretKey.error"));
+        }
+    }
+
+    public void setTextProvider(final TextProvider textProvider)
+    {
+        this.textProvider = textProvider;
+    }
+}

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

 
     <taskType name="AWS CloudFormation" class="net.utoolity.bamboo.plugins.CloudFormationTask" key="cft">
       <description>Create/Delete a CloudFormation stack.</description>
-      <!-- Categories available in 3.1: "builder", "test" and "deployment" -->
       <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>

File src/main/resources/net/utoolity/bamboo/plugins/editEC2Task.ftl

+[#if mode == "create"]
+	[@ww.radio labelKey="net.utoolity.bamboo.plugins.instanceAction" name="instanceAction" list="instanceActions" toggle="true"/]
+[#elseif mode == "edit"]
+	[@ww.label labelKey="net.utoolity.bamboo.plugins.instanceAction" name="instanceAction"/]
+[/#if]
+[@ww.select labelKey="net.utoolity.bamboo.plugins.instanceRegion" name="instanceRegion" list="instanceRegions" value="instanceRegion" required="true"/]
+[@ww.textfield labelKey="net.utoolity.bamboo.plugins.instanceId" name="instanceId" required='true'/]
+[@ui.bambooSection dependsOn='instanceAction' showOn='Stop']
+	[@ww.checkbox labelKey="net.utoolity.bamboo.plugins.forceStop" name="forceStop"/]
+[/@ui.bambooSection]
+[@ww.textfield labelKey="net.utoolity.bamboo.plugins.accessKey" name="accessKey" required='true'/]
+[#if mode == "create"]
+	[@ww.password labelKey="net.utoolity.bamboo.plugins.secretKey" name="secretKey" required='true'/]
+[#elseif mode == "edit"]
+	[@ww.checkbox labelKey="net.utoolity.bamboo.plugins.changeKey" toggle='true' name='changeKey'/]
+	[@ui.bambooSection dependsOn='changeKey' ]
+	[@ww.password labelKey="net.utoolity.bamboo.plugins.newKey" name="newKey" required='true'/]
+	[/@ui.bambooSection]
+[/#if]

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

 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.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?
+net.utoolity.bamboo.plugins.instanceAction.error = You did not select an instance action.
+net.utoolity.bamboo.plugins.instanceRegion = Instance Region
+net.utoolity.bamboo.plugins.instanceRegion.description = Which region is your instance provisioned in?
+net.utoolity.bamboo.plugins.instanceRegion.error = You did not select an instance region.
+net.utoolity.bamboo.plugins.instanceId = Instance ID
+net.utoolity.bamboo.plugins.instanceId.description = What is the ID of your provisioned instance?
+net.utoolity.bamboo.plugins.instanceId.error = You did not provide an instance ID.
+net.utoolity.bamboo.plugins.forceStop = Force stop?
+net.utoolity.bamboo.plugins.forceStop.description = Should the opportunity to flush file system caches or file system metadata be skipped? 

File src/main/resources/net/utoolity/bamboo/plugins/viewEC2Task.ftl

+[@ww.label labelKey="net.utoolity.bamboo.plugins.instanceAction" name="instanceAction"/]
+[@ww.label labelKey="net.utoolity.bamboo.plugins.instanceRegion" name="instanceRegion"/]
+[@ww.label labelKey="net.utoolity.bamboo.plugins.instanceId" name="instanceId"/]
+[@ui.bambooSection dependsOn='instanceAction' showOn='Stop']
+	[@ww.checkbox labelKey="net.utoolity.bamboo.plugins.forceStop" name="forceStop" readonly="true"/]
+[/@ui.bambooSection]
+[@ww.label labelKey="net.utoolity.bamboo.plugins.accessKey" name="accessKey"/]