/*
* Copyright (c) 2016 ingenieux Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package br.com.ingenieux.mojo.beanstalk;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
import com.amazonaws.services.ec2.model.SecurityGroup;
import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient;
import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting;
import com.amazonaws.services.elasticbeanstalk.model.DescribeApplicationsRequest;
import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
import com.amazonaws.services.elasticbeanstalk.model.OptionSpecification;
import com.amazonaws.services.elasticbeanstalk.model.SolutionStackDescription;
import org.apache.commons.collections.ComparatorUtils;
import org.apache.commons.collections.comparators.ReverseComparator;
import org.apache.commons.lang.Validate;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import br.com.ingenieux.mojo.aws.AbstractAWSMojo;
import br.com.ingenieux.mojo.aws.util.GlobUtil;
import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentCommand;
import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContext;
import br.com.ingenieux.mojo.beanstalk.cmd.env.waitfor.WaitForEnvironmentContextBuilder;
import br.com.ingenieux.mojo.beanstalk.util.ConfigUtil;
import br.com.ingenieux.mojo.beanstalk.util.EnvironmentHostnameUtil;
import static java.lang.String.format;
import static org.apache.commons.lang.StringUtils.defaultString;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;
public abstract class AbstractBeanstalkMojo extends AbstractAWSMojo<AWSElasticBeanstalkClient> {
protected List<ConfigurationOptionSetting> getOptionSettings(ConfigurationOptionSetting[] optionSettings) {
ConfigurationOptionSetting[] arrOptionSettings = optionSettings;
if (null == arrOptionSettings || 0 == arrOptionSettings.length) {
return Collections.emptyList();
}
return Arrays.asList(arrOptionSettings);
}
protected List<OptionSpecification> getOptionsToRemove(OptionSpecification[] optionSettings) {
OptionSpecification[] arrOptionSettings = optionSettings;
if (null == arrOptionSettings || 0 == arrOptionSettings.length) {
return Collections.emptyList();
}
return Arrays.asList(arrOptionSettings);
}
protected EnvironmentDescription lookupEnvironment(String applicationName, String environmentRef) throws MojoExecutionException {
final WaitForEnvironmentContext ctx =
new WaitForEnvironmentContextBuilder().withApplicationName(applicationName).withEnvironmentRef(environmentRef).build();
final Collection<EnvironmentDescription> environments = new WaitForEnvironmentCommand(this).lookupInternal(ctx);
return handleResults(environments);
}
protected EnvironmentDescription handleResults(Collection<EnvironmentDescription> environments) throws MojoExecutionException {
int len = environments.size();
if (1 == len) {
return environments.iterator().next();
}
handleNonSingle(len);
return null;
}
protected void handleNonSingle(int len) throws MojoExecutionException {
if (0 == len) {
throw new MojoExecutionException("No environments found");
} else {
throw new MojoExecutionException("Multiple environments found matching the supplied parameters (may you file a bug report?)");
}
}
/**
* Boolean predicate for harmful/placebo options <p/> I really mean harmful - If you mention a
* terminated environment settings, Elastic Beanstalk will accept, but this might lead to
* inconsistent states, specially when creating / listing environments. <p/> Trust me on this
* one.
*
* @param environmentId environment id to lookup
* @param optionSetting option setting
* @return true if this is not needed
*/
protected boolean harmfulOptionSettingP(final String environmentId, ConfigurationOptionSetting optionSetting) throws Exception {
//aws:autoscaling:launchconfiguration:SecurityGroups['sg-18585f7d']
if (ConfigUtil.optionSettingMatchesP(optionSetting, "aws:autoscaling:launchconfiguration", "SecurityGroups")) {
final String securityGroup = optionSetting.getValue();
if (-1 != securityGroup.indexOf(environmentId)) {
return true;
}
if (getLog().isInfoEnabled()) {
getLog().info("Probing security group '" + securityGroup + "'");
}
Validate.isTrue(securityGroup.matches("^sg-\\p{XDigit}{8}(,sg-\\p{XDigit}{8})*$"), "Invalid Security Group Spec: " + securityGroup);
final AmazonEC2 ec2 = this.getClientFactory().getService(AmazonEC2Client.class);
final DescribeSecurityGroupsResult describeSecurityGroupsResult =
ec2.describeSecurityGroups(new DescribeSecurityGroupsRequest().withGroupIds(securityGroup));
if (!describeSecurityGroupsResult.getSecurityGroups().isEmpty()) {
final Predicate<SecurityGroup> predicate =
new Predicate<SecurityGroup>() {
@Override
public boolean apply(SecurityGroup input) {
return -1 == input.getGroupName().indexOf(environmentId);
}
};
return Collections2.filter(describeSecurityGroupsResult.getSecurityGroups(), predicate).isEmpty();
}
}
boolean bInvalid = isBlank(optionSetting.getValue());
if (!bInvalid) {
bInvalid = (optionSetting.getNamespace().equals("aws:cloudformation:template:parameter") && optionSetting.getOptionName().equals("AppSource"));
}
if (!bInvalid) {
bInvalid = (optionSetting.getNamespace().equals("aws:elasticbeanstalk:sns:topics") && optionSetting.getOptionName().equals("Notification Topic ARN"));
}
/*
* TODO: Apply a more general regex instead
*/
if (!bInvalid && isNotBlank(environmentId)) {
bInvalid = (optionSetting.getValue().contains(environmentId));
}
return bInvalid;
}
public String lookupTemplateName(String applicationName, String templateName) {
if (!GlobUtil.hasWildcards(defaultString(templateName))) {
return templateName;
}
getLog().info(format("Template Name %s contains wildcards. A Lookup is needed", templateName));
Collection<String> configurationTemplates = getConfigurationTemplates(applicationName);
for (String configTemplateName : configurationTemplates) {
getLog().debug(format(" * Found Template Name: %s", configTemplateName));
}
/*
* TODO: Research and Review valid characters / applicable glob
* replacements
*/
Pattern templateMask = GlobUtil.globify(templateName);
for (String s : configurationTemplates) {
Matcher m = templateMask.matcher(s);
if (m.matches()) {
getLog().info(format("Selecting: %s", s));
return s;
}
}
getLog().info("Not found");
return null;
}
@SuppressWarnings("unchecked")
protected List<String> getConfigurationTemplates(String applicationName) {
List<String> configurationTemplates =
getService()
.describeApplications(new DescribeApplicationsRequest().withApplicationNames(applicationName))
.getApplications()
.get(0)
.getConfigurationTemplates();
Collections.<String>sort(configurationTemplates, new ReverseComparator(String.CASE_INSENSITIVE_ORDER));
return configurationTemplates;
}
/* TODO: Revise Suffix Dynamics */
public String ensureSuffixStripped(String cnamePrefix) {
return EnvironmentHostnameUtil.ensureSuffixStripped(cnamePrefix);
}
// TODO: Refactor w/ version lookup
@SuppressWarnings("unchecked")
protected String lookupSolutionStack(final String solutionStack) {
if (!GlobUtil.hasWildcards(solutionStack)) {
return solutionStack;
}
getLog().info("Looking up for solution stacks matching '" + solutionStack + "'");
final Function<SolutionStackDescription, String> stackTransformer =
new Function<SolutionStackDescription, String>() {
@Override
public String apply(SolutionStackDescription input) {
return input.getSolutionStackName();
}
};
final List<SolutionStackDescription> stackDetails = getService().listAvailableSolutionStacks().getSolutionStackDetails();
Collection<String> solStackList = Collections2.transform(stackDetails, stackTransformer);
final Pattern stackPattern = GlobUtil.globify(solutionStack);
List<String> matchingStacks =
new ArrayList<String>(
Collections2.filter(
solStackList,
new Predicate<String>() {
@Override
public boolean apply(String input) {
return stackPattern.matcher(input).matches();
}
}));
Collections.sort(matchingStacks, ComparatorUtils.reversedComparator(Collator.getInstance()));
if (matchingStacks.isEmpty()) {
throw new IllegalStateException("unable to lookup a solution stack matching '" + solutionStack + "'");
}
return matchingStacks.iterator().next();
}
/**
* Endpoint URL
*/
@Parameter(property = "beanstalk.endpointUrl")
protected String endpointUrl;
@Override
public AWSElasticBeanstalkClient getService() {
final AWSElasticBeanstalkClient service = super.getService();
if (isNotBlank(endpointUrl)) {
service.setEndpoint(endpointUrl);
}
return service;
}
}