1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package br.com.ingenieux.mojo.beanstalk.env;
18
19 import com.google.common.base.Predicate;
20
21 import com.amazonaws.services.elasticbeanstalk.model.CheckDNSAvailabilityRequest;
22 import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting;
23 import com.amazonaws.services.elasticbeanstalk.model.CreateEnvironmentResult;
24 import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsRequest;
25 import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsResult;
26 import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
27
28 import org.apache.commons.lang.StringUtils;
29 import org.apache.maven.plugin.AbstractMojoExecutionException;
30 import org.apache.maven.plugin.MojoExecutionException;
31 import org.apache.maven.plugin.MojoFailureException;
32 import org.apache.maven.plugins.annotations.Mojo;
33 import org.apache.maven.plugins.annotations.Parameter;
34
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Date;
38 import java.util.List;
39 import java.util.ListIterator;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42
43 import br.com.ingenieux.mojo.aws.util.CredentialsUtil;
44 import br.com.ingenieux.mojo.beanstalk.cmd.dns.BindDomainsCommand;
45 import br.com.ingenieux.mojo.beanstalk.cmd.dns.BindDomainsContext;
46 import br.com.ingenieux.mojo.beanstalk.cmd.dns.BindDomainsContextBuilder;
47 import br.com.ingenieux.mojo.beanstalk.cmd.env.swap.SwapCNamesCommand;
48 import br.com.ingenieux.mojo.beanstalk.cmd.env.swap.SwapCNamesContext;
49 import br.com.ingenieux.mojo.beanstalk.cmd.env.swap.SwapCNamesContextBuilder;
50 import br.com.ingenieux.mojo.beanstalk.cmd.env.terminate.TerminateEnvironmentCommand;
51 import br.com.ingenieux.mojo.beanstalk.cmd.env.terminate.TerminateEnvironmentContext;
52 import br.com.ingenieux.mojo.beanstalk.cmd.env.terminate.TerminateEnvironmentContextBuilder;
53 import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentCommand;
54 import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContext;
55 import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContextBuilder;
56 import br.com.ingenieux.mojo.beanstalk.util.EnvironmentHostnameUtil;
57
58 import static java.lang.String.format;
59 import static org.apache.commons.lang.StringUtils.isNotBlank;
60
61
62
63
64
65
66
67
68 @Mojo(name = "replace-environment")
69
70 public class ReplaceEnvironmentMojo extends CreateEnvironmentMojo {
71
72
73
74
75 private static final Pattern PATTERN_NUMBERED = Pattern.compile("^(.*)-(\\d+)$");
76
77
78
79
80 private static final int MAX_ENVNAME_LEN = 40;
81
82
83
84
85 @Parameter(property = "beanstalk.timeoutMins", defaultValue = "20")
86 Integer timeoutMins;
87
88
89
90
91 @Parameter(property = "beanstalk.skipIfSameVersion", defaultValue = "true")
92 boolean skipIfSameVersion = true;
93
94
95
96
97 @Parameter(property = "beanstalk.maxAttempts", defaultValue = "15")
98 Integer maxAttempts = 15;
99
100
101
102
103 @Parameter(property = "beanstalk.mockTerminateEnvironment", defaultValue = "false")
104 boolean mockTerminateEnvironment = false;
105
106
107
108
109 @Parameter(property = "beanstalk.redToRedOkay", defaultValue = "true")
110 boolean redToRedOkay = true;
111
112
113
114
115 @Parameter(property = "beanstalk.attemptRetryInterval", defaultValue = "60")
116 int attemptRetryInterval = 60;
117
118
119
120
121
122
123 @Parameter(property = "beanstalk.domains")
124 String[] domains;
125
126
127
128
129 @Parameter(property = "beanstalk.copyOptionSettings", defaultValue = "true")
130 boolean copyOptionSettings = true;
131
132
133
134
135
136 @Parameter(property = "beanstalk.copySolutionStack", defaultValue = "true")
137 boolean copySolutionStack = true;
138
139 @Override
140 protected EnvironmentDescription handleResults(Collection<EnvironmentDescription> environments) throws MojoExecutionException {
141
142
143 return null;
144 }
145
146 @Override
147 protected Object executeInternal() throws Exception {
148 solutionStack = lookupSolutionStack(solutionStack);
149
150
151
152
153
154 if (!hasEnvironmentFor(applicationName, cnamePrefix)) {
155 if (getLog().isInfoEnabled()) {
156 getLog().info("Just launching a new environment.");
157 }
158
159 return super.executeInternal();
160 }
161
162
163
164
165 EnvironmentDescription curEnv = getEnvironmentFor(applicationName, cnamePrefix);
166
167 if (curEnv.getVersionLabel().equals(versionLabel) && skipIfSameVersion) {
168 getLog().warn(format("Environment is running version %s and skipIfSameVersion is true. Returning", versionLabel));
169
170 return null;
171 }
172
173
174
175
176 String cnamePrefixToCreate = getCNamePrefixToCreate();
177
178
179 if (getLog().isInfoEnabled()) {
180 getLog().info("Creating a new environment on " + cnamePrefixToCreate + ".elasticbeanstalk.com");
181 }
182
183 if (copyOptionSettings) {
184 copyOptionSettings(curEnv);
185 }
186
187 if (!solutionStack.equals(curEnv.getSolutionStackName()) && copySolutionStack) {
188 if (getLog().isWarnEnabled()) {
189 getLog()
190 .warn(
191 format(
192 "(btw, we're launching with solutionStack/ set to '%s' based on the existing env instead of the "
193 + "default ('%s'). If this is not the desired behavior please set the copySolutionStack property to"
194 + " false.",
195 curEnv.getSolutionStackName(),
196 solutionStack));
197 }
198
199 solutionStack = curEnv.getSolutionStackName();
200 }
201
202
203
204
205 boolean userWantsHealthyExitStatus = mustBeHealthy;
206 final boolean currentEnvironmentIsRed = "Red".equals(curEnv.getHealth());
207
208 if (redToRedOkay) {
209
210 mustBeHealthy = !currentEnvironmentIsRed;
211 }
212
213
214
215
216 String newEnvironmentName = getNewEnvironmentName(StringUtils.defaultString(this.environmentName, curEnv.getEnvironmentName()));
217
218 if (getLog().isInfoEnabled()) {
219 getLog().info("And it'll be named " + newEnvironmentName);
220 getLog().info("And it will replace a '" + curEnv.getHealth() + "' enviroment");
221 }
222
223 CreateEnvironmentResult createEnvResult = createEnvironment(cnamePrefixToCreate, newEnvironmentName);
224
225
226
227
228 EnvironmentDescription newEnvDesc = null;
229
230 try {
231 newEnvDesc = waitForEnvironment(createEnvResult.getEnvironmentId());
232 } catch (Exception exc) {
233
234
235
236 terminateEnvironment(createEnvResult.getEnvironmentId());
237
238 handleException(exc);
239
240 return null;
241 }
242
243
244
245
246
247 {
248 boolean swapped = false;
249 for (int i = 1; i <= maxAttempts; i++) {
250 try {
251 swapEnvironmentCNames(newEnvDesc.getEnvironmentId(), curEnv.getEnvironmentId(), cnamePrefix, newEnvDesc);
252 swapped = true;
253 break;
254 } catch (Throwable exc) {
255 if (exc instanceof MojoFailureException) {
256 exc = Throwable.class.cast(MojoFailureException.class.cast(exc).getCause());
257 }
258
259 getLog().warn(format("Attempt #%d/%d failed. Sleeping and retrying. Reason: %s (type: %s)", i, maxAttempts, exc.getMessage(), exc.getClass()));
260
261 sleepInterval(attemptRetryInterval);
262 }
263 }
264
265 if (!swapped) {
266 getLog().info("Failed to properly Replace Environment. Finishing the new one. And throwing you a failure");
267
268 terminateEnvironment(newEnvDesc.getEnvironmentId());
269
270 String message = "Unable to swap cnames. btw, see https://github.com/ingenieux/beanstalker/issues/25 and help us improve beanstalker";
271
272 getLog().warn(message);
273
274 throw new MojoFailureException(message);
275 }
276 }
277
278
279
280
281 terminateEnvironment(curEnv.getEnvironmentId());
282
283
284
285
286 if (currentEnvironmentIsRed && userWantsHealthyExitStatus) {
287 final String newHealth = newEnvDesc.getHealth();
288
289 if (newHealth.equals("Green")) {
290 getLog().info("Previous environment was 'Red', new environment is 'Green' (for the moment)");
291 } else {
292 getLog().warn(format("Previous environment was 'Red', replacement environment is currently '%s'", newHealth));
293
294
295 newEnvDesc = waitForGreenEnvironment(createEnvResult.getEnvironmentId());
296 }
297 }
298
299 return createEnvResult;
300 }
301
302 public void sleepInterval(int pollInterval) {
303 getLog().info(format("Sleeping for %d seconds (and until %s)", pollInterval, new Date(System.currentTimeMillis() + 1000 * pollInterval)));
304 try {
305 Thread.sleep(1000 * pollInterval);
306 } catch (InterruptedException e) {
307 }
308 }
309
310
311
312
313
314
315 private void copyOptionSettings(EnvironmentDescription curEnv) throws Exception {
316
317
318
319 if (null != this.optionSettings && this.optionSettings.length > 0) {
320 return;
321 }
322
323 DescribeConfigurationSettingsResult configSettings =
324 getService()
325 .describeConfigurationSettings(
326 new DescribeConfigurationSettingsRequest().withApplicationName(applicationName).withEnvironmentName(curEnv.getEnvironmentName()));
327
328 List<ConfigurationOptionSetting> newOptionSettings =
329 new ArrayList<ConfigurationOptionSetting>(configSettings.getConfigurationSettings().get(0).getOptionSettings());
330
331 ListIterator<ConfigurationOptionSetting> listIterator = newOptionSettings.listIterator();
332
333 while (listIterator.hasNext()) {
334 ConfigurationOptionSetting curOptionSetting = listIterator.next();
335
336 boolean bInvalid = harmfulOptionSettingP(curEnv.getEnvironmentId(), curOptionSetting);
337
338 if (bInvalid) {
339 getLog()
340 .info(
341 format(
342 "Excluding Option Setting: %s:%s['%s']",
343 curOptionSetting.getNamespace(),
344 curOptionSetting.getOptionName(),
345 CredentialsUtil.redact(curOptionSetting.getValue())));
346 listIterator.remove();
347 } else {
348 getLog()
349 .info(
350 format(
351 "Including Option Setting: %s:%s['%s']",
352 curOptionSetting.getNamespace(),
353 curOptionSetting.getOptionName(),
354 CredentialsUtil.redact(curOptionSetting.getValue())));
355 }
356 }
357
358 Object __secGroups = project.getProperties().get("beanstalk.securityGroups");
359
360 if (null != __secGroups) {
361 getLog().info("Checking beanstalk.securityGroups security groups.");
362
363 String securityGroups = StringUtils.defaultString(__secGroups.toString());
364
365 if (!StringUtils.isBlank(securityGroups)) {
366 ConfigurationOptionSetting newOptionSetting = new ConfigurationOptionSetting("aws:autoscaling:launchconfiguration", "SecurityGroups", securityGroups);
367 newOptionSettings.add(newOptionSetting);
368 getLog()
369 .info(
370 format(
371 "Including Option Setting: %s:%s['%s']", newOptionSetting.getNamespace(), newOptionSetting.getOptionName(), newOptionSetting.getValue()));
372 }
373 }
374
375
376
377
378 this.optionSettings = newOptionSettings.toArray(new ConfigurationOptionSetting[newOptionSettings.size()]);
379 }
380
381
382
383
384
385
386
387 protected void swapEnvironmentCNames(String newEnvironmentId, String curEnvironmentId, String cnamePrefix, EnvironmentDescription newEnv)
388 throws AbstractMojoExecutionException {
389 getLog().info("Swapping environment cnames " + newEnvironmentId + " and " + curEnvironmentId);
390
391 {
392 SwapCNamesContext context =
393 SwapCNamesContextBuilder.swapCNamesContext()
394 .withSourceEnvironmentId(newEnvironmentId)
395 .withDestinationEnvironmentId(curEnvironmentId)
396 .build();
397 SwapCNamesCommandd/env/swap/SwapCNamesCommand.html#SwapCNamesCommand">SwapCNamesCommand command = new SwapCNamesCommand(this);
398
399 command.execute(context);
400 }
401
402
403
404
405 if (null != domains) {
406 List<String> domainsToUse = new ArrayList<String>();
407
408 for (String s : domains) {
409 if (isNotBlank(s)) {
410 domainsToUse.add(s.trim());
411 }
412 }
413
414 if (!domainsToUse.isEmpty()) {
415
416 final BindDomainsContext ctx = new BindDomainsContextBuilder().withCurEnv(newEnv).withDomains(domainsToUse).build();
417
418 new BindDomainsCommand(this).execute(ctx);
419 } else {
420 getLog().info("Skipping r53 domain binding");
421 }
422 }
423
424 {
425 WaitForEnvironmentContext context =
426 new WaitForEnvironmentContextBuilder()
427 .withApplicationName(applicationName)
428 .withStatusToWaitFor("Ready")
429 .withEnvironmentRef(newEnvironmentId)
430 .withTimeoutMins(timeoutMins)
431 .build();
432
433 WaitForEnvironmentCommanditfor/WaitForEnvironmentCommand.html#WaitForEnvironmentCommand">WaitForEnvironmentCommand command = new WaitForEnvironmentCommand(this);
434
435 command.execute(context);
436 }
437 }
438
439
440
441
442
443
444 protected void terminateEnvironment(String environmentId) throws AbstractMojoExecutionException {
445 if (mockTerminateEnvironment) {
446 getLog().info(format("We're ignoring the termination of environment id '%s' (see mockTerminateEnvironment)", environmentId));
447
448 return;
449 }
450
451 Exception lastException = null;
452 for (int i = 1; i <= maxAttempts; i++) {
453 getLog().info(format("Terminating environmentId=%s (attempt %d/%d)", environmentId, i, maxAttempts));
454
455 try {
456 TerminateEnvironmentContext terminatecontext =
457 new TerminateEnvironmentContextBuilder().withEnvironmentId(environmentId).withTerminateResources(true).build();
458 TerminateEnvironmentCommandinate/TerminateEnvironmentCommand.html#TerminateEnvironmentCommand">TerminateEnvironmentCommand command = new TerminateEnvironmentCommand(this);
459
460 command.execute(terminatecontext);
461
462 return;
463 } catch (Exception exc) {
464 lastException = exc;
465 }
466 }
467
468 throw new MojoFailureException("Unable to terminate environment " + environmentId, lastException);
469 }
470
471
472
473
474
475
476
477
478 protected EnvironmentDescription waitForEnvironment(String environmentId) throws AbstractMojoExecutionException {
479 getLog().info("Waiting for environmentId " + environmentId + " to get into Ready state");
480
481 WaitForEnvironmentContext context =
482 new WaitForEnvironmentContextBuilder()
483 .withApplicationName(applicationName)
484 .withStatusToWaitFor("Ready")
485 .withEnvironmentRef(environmentId)
486 .withHealth((mustBeHealthy ? "Green" : null))
487 .withTimeoutMins(timeoutMins)
488 .build();
489
490 WaitForEnvironmentCommanditfor/WaitForEnvironmentCommand.html#WaitForEnvironmentCommand">WaitForEnvironmentCommand command = new WaitForEnvironmentCommand(this);
491
492 return command.execute(context);
493 }
494
495
496
497
498
499
500
501
502
503
504 protected EnvironmentDescription waitForGreenEnvironment(String environmentId) throws AbstractMojoExecutionException {
505 getLog().info("Waiting for environmentId " + environmentId + " to become 'Green'");
506
507 WaitForEnvironmentContext context =
508 new WaitForEnvironmentContextBuilder()
509 .withApplicationName(applicationName)
510 .withStatusToWaitFor("Ready")
511 .withEnvironmentRef(environmentId)
512 .withHealth("Green")
513 .withTimeoutMins(timeoutMins)
514 .build();
515
516 WaitForEnvironmentCommanditfor/WaitForEnvironmentCommand.html#WaitForEnvironmentCommand">WaitForEnvironmentCommand command = new WaitForEnvironmentCommand(this);
517
518 return command.execute(context);
519 }
520
521
522
523
524
525
526 protected String getCNamePrefixToCreate() {
527 String cnamePrefixToReturn = cnamePrefix;
528 int i = 0;
529
530 while (hasEnvironmentFor(applicationName, cnamePrefixToReturn) || isNamedEnvironmentUnavailable(cnamePrefixToReturn)) {
531
532 cnamePrefixToReturn = String.format("%s-%d", cnamePrefix, i++);
533 }
534
535 return cnamePrefixToReturn;
536 }
537
538
539
540
541
542
543
544
545 protected boolean hasEnvironmentFor(String applicationName, String cnamePrefix) {
546 return null != getEnvironmentFor(applicationName, cnamePrefix);
547 }
548
549
550
551
552
553
554
555
556 protected EnvironmentDescription getEnvironmentFor(String applicationName, String cnamePrefix) {
557 Collection<EnvironmentDescription> environments = getEnvironmentsFor(applicationName);
558 Predicate<EnvironmentDescription> pred = EnvironmentHostnameUtil.getHostnamePredicate(getRegion(), cnamePrefix);
559
560
561
562
563 for (EnvironmentDescription envDesc : environments) {
564 if (pred.apply(envDesc)) {
565 return envDesc;
566 }
567 }
568
569 return null;
570 }
571
572 private String getNewEnvironmentName(String newEnvironmentName) {
573 String result = newEnvironmentName;
574 String environmentRadical = result;
575
576 int i = 0;
577
578 {
579 Matcher matcher = PATTERN_NUMBERED.matcher(newEnvironmentName);
580
581 if (matcher.matches()) {
582 environmentRadical = matcher.group(1);
583
584 i = 1 + Integer.valueOf(matcher.group(2));
585 }
586 }
587
588 while (containsNamedEnvironment(result) || isNamedEnvironmentUnavailable(result)) {
589 result = formatAndTruncate("%s-%d", MAX_ENVNAME_LEN, environmentRadical, i++);
590 }
591
592 return result;
593 }
594
595 private boolean isNamedEnvironmentUnavailable(String cnamePrefix) {
596 return !getService().checkDNSAvailability(new CheckDNSAvailabilityRequest(cnamePrefix)).isAvailable();
597 }
598
599
600
601
602
603
604
605
606
607 protected String formatAndTruncate(String mask, int maxLen, Object... args) {
608 String result = String.format(mask, args);
609
610 if (result.length() > maxLen) {
611 result = result.substring(result.length() - maxLen, result.length());
612 }
613
614 return result;
615 }
616
617
618
619
620
621
622
623 protected boolean containsNamedEnvironment(String environmentName) {
624 for (EnvironmentDescription envDesc : getEnvironmentsFor(applicationName)) {
625 if (envDesc.getEnvironmentName().equals(environmentName)) {
626 return true;
627 }
628 }
629
630 return false;
631 }
632 }