View Javadoc
1   /*
2    * Copyright (c) 2016 ingenieux Labs
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package br.com.ingenieux.mojo.beanstalk.env;
18  
19  import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting;
20  import com.amazonaws.services.elasticbeanstalk.model.CreateEnvironmentResult;
21  import com.amazonaws.services.elasticbeanstalk.model.OptionSpecification;
22  
23  import org.apache.commons.lang.Validate;
24  import org.apache.maven.plugin.AbstractMojoExecutionException;
25  import org.apache.maven.plugins.annotations.Mojo;
26  import org.apache.maven.plugins.annotations.Parameter;
27  
28  import java.util.Arrays;
29  import java.util.Collections;
30  import java.util.LinkedHashMap;
31  import java.util.Map;
32  import java.util.Properties;
33  
34  import br.com.ingenieux.mojo.beanstalk.AbstractNeedsEnvironmentMojo;
35  import br.com.ingenieux.mojo.beanstalk.cmd.env.create.CreateEnvironmentCommand;
36  import br.com.ingenieux.mojo.beanstalk.cmd.env.create.CreateEnvironmentContext;
37  import br.com.ingenieux.mojo.beanstalk.cmd.env.create.CreateEnvironmentContextBuilder;
38  import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentCommand;
39  import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContext;
40  import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContextBuilder;
41  
42  import static java.lang.String.format;
43  import static org.apache.commons.lang.StringUtils.isBlank;
44  import static org.apache.commons.lang.StringUtils.isNotBlank;
45  import static org.codehaus.plexus.util.StringUtils.defaultString;
46  
47  /**
48   * Creates and Launches an Elastic Beanstalk Environment <p> See the docs for the <a href=
49   * "http://docs.amazonwebservices.com/elasticbeanstalk/latest/api/API_CreateEnvironment.html"
50   * >CreateEnvironment API</a> call.
51   *
52   * @since 0.1.0
53   */
54  @Mojo(name = "create-environment")
55  public class CreateEnvironmentMojo extends AbstractNeedsEnvironmentMojo {
56  
57    public static final String BEANSTALK_TAG_PREFIX = "beanstalk.tag.";
58  
59    /**
60     * environmentName. Takes precedence over environmentRef.
61     */
62    @Parameter(property = "beanstalk.environmentName", required = true)
63    protected String environmentName;
64    /**
65     * Application Description
66     */
67    @Parameter(property = "beanstalk.applicationDescription", defaultValue = "${project.name}")
68    String applicationDescription;
69    /**
70     * <p> Configuration Option Settings. Will evaluate as such: </p> <p> <p> If empty, will lookup
71     * for beanstalk.env.aws.x.y variable in the context, and it will map this variable to namespace
72     * [aws.x], under option name y, unless there's an alias set. </p> <p> <p>A Property might be
73     * aliased. Current aliases include:</p> <p> <ul> <li>beanstalk.scalingAvailabilityZones, to
74     * aws:autoscaling:asg/Availability Zones</li> <li>beanstalk.scalingCooldown, to
75     * aws:autoscaling:asg/Cooldown</li> <li>beanstalk.scalingCustomAvailabilityZones, to
76     * aws:autoscaling:asg/Custom Availability Zones</li> <li>beanstalk.scalingMinSize, to
77     * aws:autoscaling:asg/MinSize</li> <li>beanstalk.scalingMaxSize, to
78     * aws:autoscaling:asg/MaxSize</li> <p> <li>beanstalk.keyName, to
79     * aws:autoscaling:launchconfiguration/EC2KeyName (EC2 Instance Key)</li>
80     * <li>beanstalk.iamInstanceProfile, to aws:autoscaling:launchconfiguration/IamInstanceProfile
81     * (IAM Instance Profile Role Name)</li> <li>beanstalk.imageId, to
82     * aws:autoscaling:launchconfiguration/ImageId</li> <li>beanstalk.instanceType, to
83     * aws:autoscaling:launchconfiguration/InstanceType (EC2 Instance Type to Use)</li>
84     * <li>beanstalk.monitoringInterval, to aws:autoscaling:launchconfiguration/MonitoringInterval</li>
85     * <li>beanstalk.securityGroups, to aws:autoscaling:launchconfiguration/SecurityGroups</li>
86     * <li>beanstalk.sshSourceRestriction, to aws:autoscaling:launchconfiguration/SSHSourceRestriction</li>
87     * <li>beanstalk.blockDeviceMappings, to aws:autoscaling:launchconfiguration/BlockDeviceMappings</li>
88     * <li>beanstalk.rootVolumeType, to aws:autoscaling:launchconfiguration/RootVolumeType</li>
89     * <li>beanstalk.rootVolumeSize, to aws:autoscaling:launchconfiguration/RootVolumeSize</li>
90     * <li>beanstalk.rootVolumeIOPS, to aws:autoscaling:launchconfiguration/RootVolumeIOPS</li> <p>
91     * <li>beanstalk.triggerBreachDuration, to aws:autoscaling:trigger/BreachDuration</li>
92     * <li>beanstalk.triggerLowerBreachScaleIncrement, to aws:autoscaling:trigger/LowerBreachScaleIncrement</li>
93     * <li>beanstalk.triggerLowerThreshold, to aws:autoscaling:trigger/LowerThreshold</li>
94     * <li>beanstalk.triggerMeasureName, to aws:autoscaling:trigger/MeasureName</li>
95     * <li>beanstalk.triggerPeriod, to aws:autoscaling:trigger/Period</li>
96     * <li>beanstalk.triggerStatistic, to aws:autoscaling:trigger/Statistic</li>
97     * <li>beanstalk.triggerUnit, to aws:autoscaling:trigger/Unit</li>
98     * <li>beanstalk.triggerUpperBreachScaleIncrement, to aws:autoscaling:trigger/UpperBreachScaleIncrement</li>
99     * <li>beanstalk.triggerUpperThreshold, to aws:autoscaling:trigger/UpperThreshold</li> <p>
100    * <li>beanstalk.rollingupdateMaxBatchSize, to aws:autoscaling:updatepolicy:rollingupdate/MaxBatchSize</li>
101    * <li>beanstalk.rollingupdateMinInstancesInService, to aws:autoscaling:updatepolicy:rollingupdate/MinInstancesInService</li>
102    * <li>beanstalk.rollingupdatePauseTime, to aws:autoscaling:updatepolicy:rollingupdate/PauseTime</li>
103    * <li>beanstalk.rollingupdateEnabled, to aws:autoscaling:updatepolicy:rollingupdate/RollingUpdateEnabled</li>
104    * <p> <li>beanstalk.vpcId, to aws:ec2:vpc/VPCId</li> <li>beanstalk.vpcSubnets, to
105    * aws:ec2:vpc/Subnets</li> <li>beanstalk.vpcELBSubnets, to aws:ec2:vpc/ELBSubnets</li>
106    * <li>beanstalk.vpcELBScheme, to aws:ec2:vpc/ELBScheme</li> <li>beanstalk.vpcDBSubnets, to
107    * aws:ec2:vpc/DBSubnets</li> <li>beanstalk.vpcAssociatePublicIpAddress, to
108    * aws:ec2:vpc/AssociatePublicIpAddress</li> <p> <li>beanstalk.applicationHealthCheckURL, to
109    * aws:elasticbeanstalk:application/Application Healthcheck URL (Application Healthcheck
110    * URL)</li> <p> <li>beanstalk.timeout, to aws:elasticbeanstalk:command/Timeout</li> <p>
111    * <li>beanstalk.environmentType, to aws:elasticbeanstalk:environment/EnvironmentType
112    * (SingleInstance or ELB-bound Environment)</li> <p> <li>beanstalk.automaticallyTerminateUnhealthyInstances,
113    * to aws:elasticbeanstalk:monitoring/Automatically Terminate Unhealthy Instances (true if
114    * should automatically terminate instances)</li> <p> <li>beanstalk.notificationEndpoint, to
115    * aws:elasticbeanstalk:sns:topics/Notification Endpoint</li> <li>beanstalk.notificationProtocol,
116    * to aws:elasticbeanstalk:sns:topics/Notification Protocol</li> <li>beanstalk.notificationTopicARN,
117    * to aws:elasticbeanstalk:sns:topics/Notification Topic ARN</li> <li>beanstalk.notificationTopicName,
118    * to aws:elasticbeanstalk:sns:topics/Notification Topic Name</li> <p>
119    * <li>beanstalk.sqsdWorkerQueueUrl, to aws:elasticbeanstalk:sqsd/WorkerQueueURL</li>
120    * <li>beanstalk.sqsdHttpPath, to aws:elasticbeanstalk:sqsd/HttpPath</li>
121    * <li>beanstalk.sqsdMimeType, to aws:elasticbeanstalk:sqsd/MimeType</li>
122    * <li>beanstalk.sqsdHttpConnections, to aws:elasticbeanstalk:sqsd/HttpConnections</li>
123    * <li>beanstalk.sqsdConnectTimeout, to aws:elasticbeanstalk:sqsd/ConnectTimeout</li>
124    * <li>beanstalk.sqsdInactivityTimeout, to aws:elasticbeanstalk:sqsd/InactivityTimeout</li>
125    * <li>beanstalk.sqsdVisibilityTimeout, to aws:elasticbeanstalk:sqsd/VisibilityTimeout</li>
126    * <li>beanstalk.sqsdRetentionPeriod, to aws:elasticbeanstalk:sqsd/RetentionPeriod</li>
127    * <li>beanstalk.sqsdMaxRetries, to aws:elasticbeanstalk:sqsd/MaxRetries</li> <p>
128    * <li>beanstalk.healthcheckHealthyThreshold, to aws:elb:healthcheck/HealthyThreshold</li>
129    * <li>beanstalk.healthcheckInterval, to aws:elb:healthcheck/Interval</li>
130    * <li>beanstalk.healthcheckTimeout, to aws:elb:healthcheck/Timeout</li>
131    * <li>beanstalk.healthcheckUnhealthyThreshold, to aws:elb:healthcheck/UnhealthyThreshold</li>
132    * <p> <li>beanstalk.loadBalancerHTTPPort, to aws:elb:loadbalancer/LoadBalancerHTTPPort</li>
133    * <li>beanstalk.loadBalancerPortProtocol, to aws:elb:loadbalancer/LoadBalancerPortProtocol</li>
134    * <li>beanstalk.loadBalancerHTTPSPort, to aws:elb:loadbalancer/LoadBalancerHTTPSPort</li>
135    * <li>beanstalk.loadBalancerSSLPortProtocol, to aws:elb:loadbalancer/LoadBalancerSSLPortProtocol</li>
136    * <li>beanstalk.loadBalancerSSLCertificateId, to aws:elb:loadbalancer/SSLCertificateId</li>
137    * <p> <li>beanstalk.stickinessCookieExpiration, to aws:elb:policies/Stickiness Cookie
138    * Expiration (Stickiness Cookie Expiration Timeout)</li> <li>beanstalk.stickinessPolicy, to
139    * aws:elb:policies/Stickiness Policy (ELB Stickiness Policy)</li> <p>
140    * <li>beanstalk.dbAllocatedStorage, to aws:rds:dbinstance/DBAllocatedStorage</li>
141    * <li>beanstalk.dbDeletionPolicy, to aws:rds:dbinstance/DBDeletionPolicy</li>
142    * <li>beanstalk.dbEngine, to aws:rds:dbinstance/DBEngine</li> <li>beanstalk.dbEngineVersion, to
143    * aws:rds:dbinstance/DBEngineVersion</li> <li>beanstalk.dbInstanceClass, to
144    * aws:rds:dbinstance/DBInstanceClass</li> <li>beanstalk.dbPassword, to
145    * aws:rds:dbinstance/DBPassword</li> <li>beanstalk.dbSnapshotIdentifier, to
146    * aws:rds:dbinstance/DBSnapshotIdentifier</li> <li>beanstalk.dbUser, to
147    * aws:rds:dbinstance/DBUser</li> <li>beanstalk.dbMultiAZDatabase, to
148    * aws:rds:dbinstance/MultiAZDatabase</li> <p> <li>beanstalk.environmentAwsSecretKey, to
149    * aws:elasticbeanstalk:application:environment/AWS_SECRET_KEY</li>
150    * <li>beanstalk.environmentAwsAccessKeyId, to aws:elasticbeanstalk:application:environment/AWS_ACCESS_KEY_ID</li>
151    * <li>beanstalk.environmentJdbcConnectionString, to aws:elasticbeanstalk:application:environment/JDBC_CONNECTION_STRING</li>
152    * <li>beanstalk.environmentParam1, to aws:elasticbeanstalk:application:environment/PARAM1</li>
153    * <li>beanstalk.environmentParam2, to aws:elasticbeanstalk:application:environment/PARAM2</li>
154    * <li>beanstalk.environmentParam3, to aws:elasticbeanstalk:application:environment/PARAM3</li>
155    * <li>beanstalk.environmentParam4, to aws:elasticbeanstalk:application:environment/PARAM4</li>
156    * <li>beanstalk.environmentParam5, to aws:elasticbeanstalk:application:environment/PARAM5</li>
157    * <p> <li>beanstalk.logPublicationControl, to aws:elasticbeanstalk:hostmanager/LogPublicationControl</li>
158    * <p> <li>beanstalk.jvmOptions, to aws:elasticbeanstalk:container:tomcat:jvmoptions/JVM
159    * Options</li> <li>beanstalk.jvmXmx, to aws:elasticbeanstalk:container:tomcat:jvmoptions/Xmx</li>
160    * <li>beanstalk.jvmMaxPermSize, to aws:elasticbeanstalk:container:tomcat:jvmoptions/XX:MaxPermSize</li>
161    * <li>beanstalk.jvmXms, to aws:elasticbeanstalk:container:tomcat:jvmoptions/Xms</li> <p>
162    * <li>beanstalk.phpDocumentRoot, to aws:elasticbeanstalk:container:php:phpini/document_root</li>
163    * <li>beanstalk.phpMemoryLimit, to aws:elasticbeanstalk:container:php:phpini/memory_limit</li>
164    * <li>beanstalk.phpZlibOutputCompression, to aws:elasticbeanstalk:container:php:phpini/zlib.output_compression</li>
165    * <li>beanstalk.phpAllowUrlFopen, to aws:elasticbeanstalk:container:php:phpini/allow_url_fopen</li>
166    * <li>beanstalk.phpDisplayErrors, to aws:elasticbeanstalk:container:php:phpini/display_errors</li>
167    * <li>beanstalk.phpMaxExecutionTime, to aws:elasticbeanstalk:container:php:phpini/max_execution_time</li>
168    * <li>beanstalk.phpComposerOptions, to aws:elasticbeanstalk:container:php:phpini/composer_options</li>
169    * </ul> <p> The reason for most of those aliases if the need to address space and ':' inside
170    * Maven Properties and XML Files.
171    */
172   @Parameter ConfigurationOptionSetting[] optionSettings;
173 
174   @Parameter OptionSpecification[] optionsToRemove;
175 
176   /**
177    * Version Label to use
178    */
179   @Parameter(property = "beanstalk.versionLabel")
180   String versionLabel;
181 
182   /**
183    * Solution Stack Name
184    */
185   @Parameter(property = "beanstalk.solutionStack", defaultValue = "32bit Amazon Linux running Tomcat 7")
186   String solutionStack;
187 
188   /**
189    * <p>If waiting for the environment to become ready, this indicates wether or not we require
190    * the Green health checks to pass.</p>
191    *
192    * <p>One reason to avoid such a requirement is if we are frantically replacing one failed
193    * environment with another one trying to get a ring of interdependant services to operate.</p>
194    *
195    * <p>Optional, but usually true. Under normal circumstances we *only* want to swap in a healthy
196    * service.</p>
197    */
198   @Parameter(property = "beanstalk.mustBeHealthy", defaultValue = "true")
199   boolean mustBeHealthy;
200 
201   /**
202    * <p>Template Name.</p> <p>
203    * <p>Could be either literal or a glob, like, <pre>ingenieux-services-prod-*</pre>. If a glob,
204    * there will be a lookup involved, and the first one in reverse ASCIIbetical order will be
205    * picked upon. </p>
206    */
207   @Parameter(property = "beanstalk.templateName")
208   String templateName;
209 
210   /**
211    * <p>Status to Wait For</p> <p> <p>Optional. If set, will block until app status is set eg
212    * "Ready"</p>
213    */
214   @Parameter(property = "beanstalk.waitForReady", defaultValue = "true")
215   boolean waitForReady;
216 
217   /**
218    * <p>Environment Tier Name (defaults to "WebServer")</p>
219    */
220   @Parameter(property = "beanstalk.environmentTierName", defaultValue = "WebServer")
221   String environmentTierName;
222 
223   /**
224    * <p>Environment Tier Type</p>
225    */
226   @Parameter(property = "beanstalk.environmentTierType")
227   String environmentTierType;
228 
229   /**
230    * <p>Environment Tier Version</p>
231    */
232   @Parameter(property = "beanstalk.environmentTierVersion")
233   String environmentTierVersion;
234 
235   /**
236    * <p>CNAME Prefix</p>
237    */
238   @Parameter(property = "beanstalk.cnamePrefix")
239   String cnamePrefix;
240 
241   /**
242    * <p>Environment Tag List</p> <p> To set up environment tags put this under plugin
243    * configuration tag
244    * <pre>
245    * {@code
246    * <tags>
247    *   <tagName>tagValue</tagName>
248    * </tags>
249    * }
250    * </pre>
251    * or use system property in the following format: beanstalk.tag.TAG_NAME=TAG_VALUE
252    */
253   @Parameter Map<String, String> tags = Collections.emptyMap();
254 
255   /**
256    * Final Computed Tags
257    */
258   Map<String, String> tagsToUse = Collections.emptyMap();
259 
260   /**
261    * Overrides parent in order to avoid a thrown exception as there's not an environment to
262    * lookup
263    */
264   @Override
265   protected void configure() {
266     projectTags();
267   }
268 
269   /**
270    * Compute the Tags to USe
271    *
272    * TODO: Do we need system.properties?
273    */
274   protected Map<String, String> projectTags() {
275     tagsToUse = new LinkedHashMap<String, String>(this.tags);
276 
277     /**
278      * First: Cleanup
279      */
280     for (Map.Entry<String, String> e : tagsToUse.entrySet()) {
281       String k = e.getKey();
282 
283       String v = e.getValue();
284 
285       if (isBlank(v)) {
286         getLog().info(format("Excluding tag: '%s'", k));
287         tagsToUse.remove(k);
288       }
289     }
290 
291     // Step #2: Context
292 
293     Properties contextProperties = new Properties();
294 
295     for (Map.Entry<Object, Object> e : context.getContextData().entrySet()) {
296       if (!("" + e.getKey()).startsWith(BEANSTALK_TAG_PREFIX)) continue;
297 
298       contextProperties.put(e.getKey(), e.getValue());
299     }
300 
301     // Now consolidate all three
302 
303     for (Properties props : Arrays.asList(project.getProperties(), contextProperties, System.getProperties())) {
304       for (Map.Entry<Object, Object> e : project.getProperties().entrySet()) {
305         String k = "" + e.getKey();
306 
307         if (!k.startsWith(BEANSTALK_TAG_PREFIX)) continue;
308 
309         k = k.substring(BEANSTALK_TAG_PREFIX.length());
310 
311         String v = defaultString(e.getValue());
312 
313         if (isBlank(v)) {
314           getLog().info(format("Excluding tag: '%s'", k));
315           tagsToUse.remove(k);
316         } else {
317           getLog().info(format("Assigning Tag: '%s'='%s'", k, v));
318           tagsToUse.put(k, v);
319         }
320       }
321     }
322 
323     return tagsToUse;
324   }
325 
326   @Override
327   protected Object executeInternal() throws Exception {
328     versionLabel = lookupVersionLabel(applicationName, versionLabel);
329 
330     return createEnvironment(cnamePrefix, environmentName);
331   }
332 
333   protected CreateEnvironmentResult createEnvironment(String cnameToCreate, String newEnvironmentName) throws AbstractMojoExecutionException {
334     /*
335      * Hey Aldrin, have you ever noticed we're getting pedantic on those validations?
336      */
337     Validate.isTrue(isNotBlank(newEnvironmentName), "No New Environment Name Supplied");
338 
339     if (null == optionSettings) {
340       optionSettings = introspectOptionSettings();
341     }
342 
343     versionLabel = lookupVersionLabel(applicationName, versionLabel);
344 
345     CreateEnvironmentContextBuilder builder =
346         CreateEnvironmentContextBuilder.createEnvironmentContext() //
347             .withApplicationName(applicationName) //
348             .withApplicationDescription(applicationDescription) //
349             .withCnamePrefix(cnameToCreate) //
350             .withSolutionStack(lookupSolutionStack(solutionStack)) //
351             .withTemplateName(templateName) //
352             .withEnvironmentName(newEnvironmentName) //
353             .withOptionSettings(optionSettings) //
354             .withOptionsToRemove(optionsToRemove) //
355             .withEnvironmentTierName(environmentTierName) //
356             .withEnvironmentTierType(environmentTierType) //
357             .withEnvironmentTierVersion(environmentTierVersion) //
358             .withVersionLabel(versionLabel) //
359             .withTags(tagsToUse); //
360 
361     CreateEnvironmentContext context = builder.build();
362 
363     CreateEnvironmentCommandreate/CreateEnvironmentCommand.html#CreateEnvironmentCommand">CreateEnvironmentCommand command = new CreateEnvironmentCommand(this);
364 
365     CreateEnvironmentResult result = command.execute(context);
366 
367     if (waitForReady) {
368       WaitForEnvironmentContext ctx =
369           new WaitForEnvironmentContextBuilder() //
370               .withEnvironmentRef(result.getEnvironmentId()) //
371               .withApplicationName(result.getApplicationName()) //
372               .withHealth(mustBeHealthy ? "Green" : null) //
373               .withStatusToWaitFor("Ready") //
374               .build();
375 
376       new WaitForEnvironmentCommand(this).execute(ctx);
377     }
378 
379     return result;
380   }
381 }