/*
* 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.cmd.dns;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting;
import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsRequest;
import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsResult;
import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentResourcesRequest;
import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancing;
import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient;
import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest;
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;
import com.amazonaws.services.route53.AmazonRoute53;
import com.amazonaws.services.route53.AmazonRoute53Client;
import com.amazonaws.services.route53.model.AliasTarget;
import com.amazonaws.services.route53.model.Change;
import com.amazonaws.services.route53.model.ChangeAction;
import com.amazonaws.services.route53.model.ChangeBatch;
import com.amazonaws.services.route53.model.ChangeResourceRecordSetsRequest;
import com.amazonaws.services.route53.model.HostedZone;
import com.amazonaws.services.route53.model.ListResourceRecordSetsRequest;
import com.amazonaws.services.route53.model.ListResourceRecordSetsResult;
import com.amazonaws.services.route53.model.RRType;
import com.amazonaws.services.route53.model.ResourceRecord;
import com.amazonaws.services.route53.model.ResourceRecordSet;
import org.apache.commons.lang.Validate;
import org.apache.maven.plugin.AbstractMojoExecutionException;
import org.apache.maven.plugin.MojoExecutionException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import br.com.ingenieux.mojo.beanstalk.AbstractNeedsEnvironmentMojo;
import br.com.ingenieux.mojo.beanstalk.cmd.BaseCommand;
import br.com.ingenieux.mojo.beanstalk.util.ConfigUtil;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.apache.commons.lang.StringUtils.join;
import static org.apache.commons.lang.StringUtils.strip;
public class BindDomainsCommand extends BaseCommand<BindDomainsContext, Void> {
private final AmazonRoute53 r53;
private final AmazonEC2 ec2;
private final AmazonElasticLoadBalancing elb;
/**
* Constructor
*
* @param parentMojo parent mojo
*/
public BindDomainsCommand(AbstractNeedsEnvironmentMojo parentMojo) throws AbstractMojoExecutionException {
super(parentMojo);
try {
this.r53 = parentMojo.getClientFactory().getService(AmazonRoute53Client.class);
this.ec2 = parentMojo.getClientFactory().getService(AmazonEC2Client.class);
this.elb = parentMojo.getClientFactory().getService(AmazonElasticLoadBalancingClient.class);
} catch (Exception exc) {
throw new MojoExecutionException("Failure", exc);
}
}
protected boolean isSingleInstance(EnvironmentDescription env) {
Validate.isTrue("WebServer".equals(env.getTier().getName()), "Not a Web Server environment!");
final DescribeConfigurationSettingsResult describeConfigurationSettingsResult =
parentMojo
.getService()
.describeConfigurationSettings(
new DescribeConfigurationSettingsRequest().withApplicationName(env.getApplicationName()).withEnvironmentName(env.getEnvironmentName()));
Validate.isTrue(1 == describeConfigurationSettingsResult.getConfigurationSettings().size(), "There should be one environment");
final List<ConfigurationOptionSetting> optionSettings = describeConfigurationSettingsResult.getConfigurationSettings().get(0).getOptionSettings();
for (ConfigurationOptionSetting optionSetting : optionSettings) {
if (ConfigUtil.optionSettingMatchesP(optionSetting, "aws:elasticbeanstalk:environment", "EnvironmentType")) {
return "SingleInstance".equals(optionSetting.getValue());
}
}
throw new IllegalStateException("Unreachable code!");
}
@Override
protected Void executeInternal(BindDomainsContext ctx) throws Exception {
Map<String, String> recordsToAssign = new LinkedHashMap<String, String>();
ctx.singleInstance = isSingleInstance(ctx.getCurEnv());
/**
* Step #2: Validate Parameters
*/
{
for (String domain : ctx.getDomains()) {
String key = formatDomain(domain);
String value = null;
/*
* Handle Entries in the form <record>:<zoneid>
*/
if (-1 != key.indexOf(':')) {
String[] pair = key.split(":", 2);
key = formatDomain(pair[0]);
value = strip(pair[1], ".");
}
recordsToAssign.put(key, value);
}
Validate.isTrue(recordsToAssign.size() > 0, "No Domains Supplied!");
if (isInfoEnabled()) {
info("Domains to Map to Environment (cnamePrefix='%s')", ctx.getCurEnv().getCNAME());
for (Map.Entry<String, String> entry : recordsToAssign.entrySet()) {
String key = entry.getKey();
String zoneId = entry.getValue();
String message = format(" * Domain: %s", key);
if (null != zoneId) {
message += " (and using zoneId " + zoneId + ")";
}
info(message);
}
}
}
/**
* Step #3: Lookup Domains on Route53
*/
Map<String, HostedZone> hostedZoneMapping = new LinkedHashMap<String, HostedZone>();
{
Set<String> unresolvedDomains = new LinkedHashSet<String>();
for (Map.Entry<String, String> entry : recordsToAssign.entrySet()) {
if (null != entry.getValue()) {
continue;
}
unresolvedDomains.add(entry.getKey());
}
for (HostedZone hostedZone : r53.listHostedZones().getHostedZones()) {
String id = hostedZone.getId();
String name = hostedZone.getName();
hostedZoneMapping.put(id, hostedZone);
if (unresolvedDomains.contains(name)) {
if (isInfoEnabled()) {
info("Mapping Domain %s to R53 Zone Id %s", name, id);
}
recordsToAssign.put(name, id);
unresolvedDomains.remove(name);
}
}
Validate.isTrue(unresolvedDomains.isEmpty(), "Domains not resolved: " + join(unresolvedDomains, "; "));
}
/**
* Step #4: Domain Validation
*/
{
for (Map.Entry<String, String> entry : recordsToAssign.entrySet()) {
String record = entry.getKey();
String zoneId = entry.getValue();
HostedZone hostedZone = hostedZoneMapping.get(zoneId);
Validate.notNull(hostedZone, format("Unknown Hosted Zone Id: %s for Record: %s", zoneId, record));
Validate.isTrue(
record.endsWith(hostedZone.getName()), format("Record %s does not map to zoneId %s (domain: %s)", record, zoneId, hostedZone.getName()));
}
}
/**
* Step #5: Get ELB Hosted Zone Id - if appliable
*/
if (!ctx.singleInstance) {
String loadBalancerName =
parentMojo
.getService()
.describeEnvironmentResources(new DescribeEnvironmentResourcesRequest().withEnvironmentId(ctx.getCurEnv().getEnvironmentId()))
.getEnvironmentResources()
.getLoadBalancers()
.get(0)
.getName();
DescribeLoadBalancersRequest req = new DescribeLoadBalancersRequest(asList(loadBalancerName));
List<LoadBalancerDescription> loadBalancers = elb.describeLoadBalancers(req).getLoadBalancerDescriptions();
Validate.isTrue(1 == loadBalancers.size(), "Unexpected number of Load Balancers returned");
ctx.elbHostedZoneId = loadBalancers.get(0).getCanonicalHostedZoneNameID();
if (isInfoEnabled()) {
info(format("Using ELB Canonical Hosted Zone Name Id %s", ctx.elbHostedZoneId));
}
}
/**
* Step #6: Apply Change Batch on Each Domain
*/
for (Map.Entry<String, String> recordEntry : recordsToAssign.entrySet()) {
assignDomain(ctx, recordEntry.getKey(), recordEntry.getValue());
}
return null;
}
protected void assignDomain(BindDomainsContext ctx, String record, String zoneId) {
ChangeBatch changeBatch = new ChangeBatch();
changeBatch.setComment(format("Updated for env %s", ctx.getCurEnv().getCNAME()));
/**
* Look for Existing Resource Record Sets
*/
{
ResourceRecordSet resourceRecordSet = null;
ListResourceRecordSetsResult listResourceRecordSets = r53.listResourceRecordSets(new ListResourceRecordSetsRequest(zoneId));
for (ResourceRecordSet rrs : listResourceRecordSets.getResourceRecordSets()) {
if (!rrs.getName().equals(record)) {
continue;
}
boolean matchesTypes = "A".equals(rrs.getType()) || "CNAME".equals(rrs.getType());
if (!matchesTypes) {
continue;
}
if (isInfoEnabled()) {
info("Excluding resourceRecordSet %s for domain %s", rrs, record);
}
changeBatch.getChanges().add(new Change(ChangeAction.DELETE, rrs));
}
}
/**
* Then Add Ours
*/
ResourceRecordSet resourceRecordSet = new ResourceRecordSet();
resourceRecordSet.setName(record);
resourceRecordSet.setType(RRType.A);
if (ctx.singleInstance) {
final String address = ctx.getCurEnv().getEndpointURL();
ResourceRecord resourceRecord = new ResourceRecord(address);
resourceRecordSet.setTTL(60L);
resourceRecordSet.setResourceRecords(asList(resourceRecord));
if (isInfoEnabled()) {
info("Adding resourceRecordSet %s for domain %s mapped to %s", resourceRecordSet, record, address);
}
} else {
AliasTarget aliasTarget = new AliasTarget();
aliasTarget.setHostedZoneId(ctx.getElbHostedZoneId());
aliasTarget.setDNSName(ctx.getCurEnv().getEndpointURL());
resourceRecordSet.setAliasTarget(aliasTarget);
if (isInfoEnabled()) {
info("Adding resourceRecordSet %s for domain %s mapped to %s", resourceRecordSet, record, aliasTarget.getDNSName());
}
}
changeBatch.getChanges().add(new Change(ChangeAction.CREATE, resourceRecordSet));
if (isInfoEnabled()) {
info("Changes to be sent: %s", changeBatch.getChanges());
}
ChangeResourceRecordSetsRequest req = new ChangeResourceRecordSetsRequest(zoneId, changeBatch);
r53.changeResourceRecordSets(req);
}
String formatDomain(String d) {
return strip(d, ".").concat(".");
}
}