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;
18  
19  import static java.lang.String.format;
20  import static org.apache.commons.lang.StringUtils.defaultString;
21  import static org.apache.commons.lang.StringUtils.isBlank;
22  import static org.apache.commons.lang.StringUtils.isNotBlank;
23  
24  import java.text.Collator;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  import java.util.stream.Collectors;
33  
34  import org.apache.commons.collections.ComparatorUtils;
35  import org.apache.commons.collections.comparators.ReverseComparator;
36  import org.apache.commons.lang.Validate;
37  import org.apache.maven.plugin.MojoExecutionException;
38  import org.apache.maven.plugins.annotations.Parameter;
39  
40  import com.amazonaws.services.ec2.AmazonEC2;
41  import com.amazonaws.services.ec2.AmazonEC2Client;
42  import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
43  import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
44  import com.amazonaws.services.ec2.model.SecurityGroup;
45  import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient;
46  import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting;
47  import com.amazonaws.services.elasticbeanstalk.model.DescribeApplicationsRequest;
48  import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
49  import com.amazonaws.services.elasticbeanstalk.model.OptionSpecification;
50  import com.amazonaws.services.elasticbeanstalk.model.SolutionStackDescription;
51  import com.google.common.base.Function;
52  import com.google.common.base.Predicate;
53  import com.google.common.collect.Collections2;
54  
55  import br.com.ingenieux.mojo.aws.AbstractAWSMojo;
56  import br.com.ingenieux.mojo.aws.util.GlobUtil;
57  import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentCommand;
58  import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContext;
59  import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContextBuilder;
60  import br.com.ingenieux.mojo.beanstalk.util.ConfigUtil;
61  import br.com.ingenieux.mojo.beanstalk.util.EnvironmentHostnameUtil;
62  
63  public abstract class AbstractBeanstalkMojo extends AbstractAWSMojo<AWSElasticBeanstalkClient> {
64  
65    protected List<ConfigurationOptionSetting> getOptionSettings(ConfigurationOptionSetting[] optionSettings) {
66      ConfigurationOptionSetting[] arrOptionSettings = optionSettings;
67  
68      if (null == arrOptionSettings || 0 == arrOptionSettings.length) {
69        return Collections.emptyList();
70      }
71  
72      return Arrays.asList(arrOptionSettings);
73    }
74  
75    protected List<OptionSpecification> getOptionsToRemove(OptionSpecification[] optionSettings) {
76      OptionSpecification[] arrOptionSettings = optionSettings;
77  
78      if (null == arrOptionSettings || 0 == arrOptionSettings.length) {
79        return Collections.emptyList();
80      }
81  
82      return Arrays.asList(arrOptionSettings);
83    }
84  
85    protected EnvironmentDescription lookupEnvironment(String applicationName, String environmentRef) throws MojoExecutionException {
86      final WaitForEnvironmentContext ctx =
87          new WaitForEnvironmentContextBuilder().withApplicationName(applicationName).withEnvironmentRef(environmentRef).build();
88      final Collection<EnvironmentDescription> environments = new WaitForEnvironmentCommand(this).lookupInternal(ctx);
89      return handleResults(environments);
90    }
91  
92    protected EnvironmentDescription handleResults(Collection<EnvironmentDescription> environments) throws MojoExecutionException {
93      int len = environments.size();
94  
95      if (1 == len) {
96        return environments.iterator().next();
97      }
98  
99      handleNonSingle(len);
100 
101     return null;
102   }
103 
104   protected void handleNonSingle(int len) throws MojoExecutionException {
105     if (0 == len) {
106       throw new MojoExecutionException("No environments found");
107     } else {
108       throw new MojoExecutionException("Multiple environments found matching the supplied parameters (may you file a bug report?)");
109     }
110   }
111 
112   /**
113    * Boolean predicate for harmful/placebo options <p> I really mean harmful - If you mention a
114    * terminated environment settings, Elastic Beanstalk will accept, but this might lead to
115    * inconsistent states, specially when creating / listing environments. <p> Trust me on this
116    * one.
117    *
118    * @param environmentId environment id to lookup
119    * @param optionSetting option setting
120    * @return true if this is not needed
121    */
122   protected boolean harmfulOptionSettingP(final String environmentId, ConfigurationOptionSetting optionSetting) throws Exception {
123     //aws:autoscaling:launchconfiguration:SecurityGroups['sg-18585f7d']
124     if (ConfigUtil.optionSettingMatchesP(optionSetting, "aws:autoscaling:launchconfiguration", "SecurityGroups")) {
125       final String securityGroup = optionSetting.getValue();
126 
127       if (-1 != securityGroup.indexOf(environmentId)) {
128         return true;
129       }
130 
131       if (getLog().isInfoEnabled()) {
132         getLog().info("Probing security group '" + securityGroup + "'");
133       }
134 
135       Validate.isTrue(securityGroup.matches("^sg-\\p{XDigit}{8,17}(,sg-\\p{XDigit}{8,17})*$"), "Invalid Security Group Spec: " + securityGroup);
136 
137       final AmazonEC2 ec2 = this.getClientFactory().getService(AmazonEC2Client.class);
138       
139       final DescribeSecurityGroupsResult describeSecurityGroupsResult =
140           ec2.describeSecurityGroups(new DescribeSecurityGroupsRequest().withGroupIds(securityGroup.split(",")));
141 
142       if (!describeSecurityGroupsResult.getSecurityGroups().isEmpty()) {
143     	  
144     	getLog().info("Checking security groups.");
145     	
146         final Predicate<SecurityGroup> predicate = new Predicate<SecurityGroup>() {
147 		  @Override
148 		  public boolean apply(SecurityGroup input) {
149 			
150 			  getLog().info("Checking predicate on security group " + input.getGroupName() + ", index of " + environmentId + " is " + input.getGroupName().indexOf(environmentId));
151 			
152 		    return -1 == input.getGroupName().indexOf(environmentId);
153 		  }
154         };
155 
156         boolean flag = Collections2.filter(describeSecurityGroupsResult.getSecurityGroups(), predicate).isEmpty();
157         
158         
159         // issue 128 remove non-custom security groups from the list of security groups
160         List<String> customSecurityGroups = describeSecurityGroupsResult.getSecurityGroups().stream().filter(sg -> {
161         	return !sg.getGroupName().contains(environmentId);
162         }).map(csg -> csg.getGroupId()).collect(Collectors.toList());
163         
164         String securityGroupCsv = String.join(",", customSecurityGroups);
165         getLog().info("Adjusting security group from " + optionSetting.getValue() + " to " + securityGroupCsv);
166 
167         getLog().info("returning  " + flag + " for security group check.");
168 
169         optionSetting.setValue(securityGroupCsv);
170         
171         return flag;
172       }
173     }
174 
175     boolean bInvalid = isBlank(optionSetting.getValue());
176 
177     if (!bInvalid) {
178       bInvalid = (optionSetting.getNamespace().equals("aws:cloudformation:template:parameter") && optionSetting.getOptionName().equals("AppSource"));
179     }
180 
181     if (!bInvalid) {
182       bInvalid = (optionSetting.getNamespace().equals("aws:elasticbeanstalk:sns:topics") && optionSetting.getOptionName().equals("Notification Topic ARN"));
183     }
184 
185     /*
186      * TODO: Apply a more general regex instead
187      */
188     if (!bInvalid && isNotBlank(environmentId)) {
189       bInvalid = (optionSetting.getValue().contains(environmentId));
190     }
191 
192     return bInvalid;
193   }
194 
195   public String lookupTemplateName(String applicationName, String templateName) {
196     if (!GlobUtil.hasWildcards(defaultString(templateName))) {
197       return templateName;
198     }
199 
200     getLog().info(format("Template Name %s contains wildcards. A Lookup is needed", templateName));
201 
202     Collection<String> configurationTemplates = getConfigurationTemplates(applicationName);
203 
204     for (String configTemplateName : configurationTemplates) {
205       getLog().debug(format(" * Found Template Name: %s", configTemplateName));
206     }
207 
208     /*
209      * TODO: Research and Review valid characters / applicable glob
210      * replacements
211      */
212     Pattern templateMask = GlobUtil.globify(templateName);
213 
214     for (String s : configurationTemplates) {
215       Matcher m = templateMask.matcher(s);
216       if (m.matches()) {
217         getLog().info(format("Selecting: %s", s));
218         return s;
219       }
220     }
221 
222     getLog().info("Not found");
223 
224     return null;
225   }
226 
227   @SuppressWarnings("unchecked")
228   protected List<String> getConfigurationTemplates(String applicationName) {
229     List<String> configurationTemplates =
230         getService()
231             .describeApplications(new DescribeApplicationsRequest().withApplicationNames(applicationName))
232             .getApplications()
233             .get(0)
234             .getConfigurationTemplates();
235 
236     Collections.<String>sort(configurationTemplates, new ReverseComparator(String.CASE_INSENSITIVE_ORDER));
237 
238     return configurationTemplates;
239   }
240 
241   /* TODO: Revise Suffix Dynamics */
242   public String ensureSuffixStripped(String cnamePrefix) {
243     return EnvironmentHostnameUtil.ensureSuffixStripped(cnamePrefix);
244   }
245 
246   // TODO: Refactor w/ version lookup
247   @SuppressWarnings("unchecked")
248   protected String lookupSolutionStack(final String solutionStack) {
249     if (!GlobUtil.hasWildcards(solutionStack)) {
250       return solutionStack;
251     }
252 
253     getLog().info("Looking up for solution stacks matching '" + solutionStack + "'");
254 
255     final Function<SolutionStackDescription, String> stackTransformer =
256         new Function<SolutionStackDescription, String>() {
257           @Override
258           public String apply(SolutionStackDescription input) {
259             return input.getSolutionStackName();
260           }
261         };
262     final List<SolutionStackDescription> stackDetails = getService().listAvailableSolutionStacks().getSolutionStackDetails();
263 
264     Collection<String> solStackList = Collections2.transform(stackDetails, stackTransformer);
265 
266     final Pattern stackPattern = GlobUtil.globify(solutionStack);
267 
268     List<String> matchingStacks =
269         new ArrayList<String>(
270             Collections2.filter(
271                 solStackList,
272                 new Predicate<String>() {
273                   @Override
274                   public boolean apply(String input) {
275                     return stackPattern.matcher(input).matches();
276                   }
277                 }));
278 
279     Collections.sort(matchingStacks, ComparatorUtils.reversedComparator(Collator.getInstance()));
280 
281     if (matchingStacks.isEmpty()) {
282       throw new IllegalStateException("unable to lookup a solution stack matching '" + solutionStack + "'");
283     }
284 
285     return matchingStacks.iterator().next();
286   }
287 
288   /**
289    * Endpoint URL
290    */
291   @Parameter(property = "beanstalk.endpointUrl")
292   protected String endpointUrl;
293 
294   @Override
295   public AWSElasticBeanstalkClient getService() {
296     final AWSElasticBeanstalkClient service = super.getService();
297 
298     if (isNotBlank(endpointUrl)) {
299       service.setEndpoint(endpointUrl);
300     }
301 
302     return service;
303   }
304 }