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.cmd.dns;
18  
19  import com.amazonaws.services.ec2.AmazonEC2;
20  import com.amazonaws.services.ec2.AmazonEC2Client;
21  import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting;
22  import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsRequest;
23  import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsResult;
24  import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentResourcesRequest;
25  import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
26  import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancing;
27  import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient;
28  import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest;
29  import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;
30  import com.amazonaws.services.route53.AmazonRoute53;
31  import com.amazonaws.services.route53.AmazonRoute53Client;
32  import com.amazonaws.services.route53.model.AliasTarget;
33  import com.amazonaws.services.route53.model.Change;
34  import com.amazonaws.services.route53.model.ChangeAction;
35  import com.amazonaws.services.route53.model.ChangeBatch;
36  import com.amazonaws.services.route53.model.ChangeResourceRecordSetsRequest;
37  import com.amazonaws.services.route53.model.HostedZone;
38  import com.amazonaws.services.route53.model.ListResourceRecordSetsRequest;
39  import com.amazonaws.services.route53.model.ListResourceRecordSetsResult;
40  import com.amazonaws.services.route53.model.RRType;
41  import com.amazonaws.services.route53.model.ResourceRecord;
42  import com.amazonaws.services.route53.model.ResourceRecordSet;
43  
44  import org.apache.commons.lang.Validate;
45  import org.apache.maven.plugin.AbstractMojoExecutionException;
46  import org.apache.maven.plugin.MojoExecutionException;
47  
48  import java.util.LinkedHashMap;
49  import java.util.LinkedHashSet;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Set;
53  
54  import br.com.ingenieux.mojo.beanstalk.AbstractNeedsEnvironmentMojo;
55  import br.com.ingenieux.mojo.beanstalk.cmd.BaseCommand;
56  import br.com.ingenieux.mojo.beanstalk.util.ConfigUtil;
57  
58  import static java.lang.String.format;
59  import static java.util.Arrays.asList;
60  import static org.apache.commons.lang.StringUtils.join;
61  import static org.apache.commons.lang.StringUtils.strip;
62  
63  public class BindDomainsCommand extends BaseCommand<BindDomainsContext, Void> {
64  
65    private final AmazonRoute53 r53;
66    private final AmazonEC2 ec2;
67    private final AmazonElasticLoadBalancing elb;
68  
69    /**
70     * Constructor
71     *
72     * @param parentMojo parent mojo
73     */
74    public BindDomainsCommand(AbstractNeedsEnvironmentMojo parentMojo) throws AbstractMojoExecutionException {
75      super(parentMojo);
76  
77      try {
78        this.r53 = parentMojo.getClientFactory().getService(AmazonRoute53Client.class);
79        this.ec2 = parentMojo.getClientFactory().getService(AmazonEC2Client.class);
80        this.elb = parentMojo.getClientFactory().getService(AmazonElasticLoadBalancingClient.class);
81  
82      } catch (Exception exc) {
83        throw new MojoExecutionException("Failure", exc);
84      }
85    }
86  
87    protected boolean isSingleInstance(EnvironmentDescription env) {
88      Validate.isTrue("WebServer".equals(env.getTier().getName()), "Not a Web Server environment!");
89  
90      final DescribeConfigurationSettingsResult describeConfigurationSettingsResult =
91          parentMojo
92              .getService()
93              .describeConfigurationSettings(
94                  new DescribeConfigurationSettingsRequest().withApplicationName(env.getApplicationName()).withEnvironmentName(env.getEnvironmentName()));
95  
96      Validate.isTrue(1 == describeConfigurationSettingsResult.getConfigurationSettings().size(), "There should be one environment");
97  
98      final List<ConfigurationOptionSetting> optionSettings = describeConfigurationSettingsResult.getConfigurationSettings().get(0).getOptionSettings();
99  
100     for (ConfigurationOptionSetting optionSetting : optionSettings) {
101       if (ConfigUtil.optionSettingMatchesP(optionSetting, "aws:elasticbeanstalk:environment", "EnvironmentType")) {
102         return "SingleInstance".equals(optionSetting.getValue());
103       }
104     }
105 
106     throw new IllegalStateException("Unreachable code!");
107   }
108 
109   @Override
110   protected Void executeInternal(BindDomainsContext ctx) throws Exception {
111     Map<String, String> recordsToAssign = new LinkedHashMap<String, String>();
112 
113     ctx.singleInstance = isSingleInstance(ctx.getCurEnv());
114 
115     /**
116      * Step #2: Validate Parameters
117      */
118     {
119       for (String domain : ctx.getDomains()) {
120         String key = formatDomain(domain);
121         String value = null;
122 
123         /*
124          * Handle Entries in the form <record>:<zoneid>
125          */
126         if (-1 != key.indexOf(':')) {
127           String[] pair = key.split(":", 2);
128 
129           key = formatDomain(pair[0]);
130           value = strip(pair[1], ".");
131         }
132 
133         recordsToAssign.put(key, value);
134       }
135 
136       Validate.isTrue(recordsToAssign.size() > 0, "No Domains Supplied!");
137 
138       if (isInfoEnabled()) {
139         info("Domains to Map to Environment (cnamePrefix='%s')", ctx.getCurEnv().getCNAME());
140 
141         for (Map.Entry<String, String> entry : recordsToAssign.entrySet()) {
142           String key = entry.getKey();
143           String zoneId = entry.getValue();
144 
145           String message = format(" * Domain: %s", key);
146 
147           if (null != zoneId) {
148             message += " (and using zoneId " + zoneId + ")";
149           }
150 
151           info(message);
152         }
153       }
154     }
155 
156     /**
157      * Step #3: Lookup Domains on Route53
158      */
159     Map<String, HostedZone> hostedZoneMapping = new LinkedHashMap<String, HostedZone>();
160 
161     {
162       Set<String> unresolvedDomains = new LinkedHashSet<String>();
163 
164       for (Map.Entry<String, String> entry : recordsToAssign.entrySet()) {
165         if (null != entry.getValue()) {
166           continue;
167         }
168 
169         unresolvedDomains.add(entry.getKey());
170       }
171 
172       for (HostedZone hostedZone : r53.listHostedZones().getHostedZones()) {
173         String id = hostedZone.getId();
174         String name = hostedZone.getName();
175 
176         hostedZoneMapping.put(id, hostedZone);
177 
178         if (unresolvedDomains.contains(name)) {
179           if (isInfoEnabled()) {
180             info("Mapping Domain %s to R53 Zone Id %s", name, id);
181           }
182 
183           recordsToAssign.put(name, id);
184 
185           unresolvedDomains.remove(name);
186         }
187       }
188 
189       Validate.isTrue(unresolvedDomains.isEmpty(), "Domains not resolved: " + join(unresolvedDomains, "; "));
190     }
191 
192     /**
193      * Step #4: Domain Validation
194      */
195     {
196       for (Map.Entry<String, String> entry : recordsToAssign.entrySet()) {
197         String record = entry.getKey();
198         String zoneId = entry.getValue();
199         HostedZone hostedZone = hostedZoneMapping.get(zoneId);
200 
201         Validate.notNull(hostedZone, format("Unknown Hosted Zone Id: %s for Record: %s", zoneId, record));
202         Validate.isTrue(
203             record.endsWith(hostedZone.getName()), format("Record %s does not map to zoneId %s (domain: %s)", record, zoneId, hostedZone.getName()));
204       }
205     }
206 
207     /**
208      * Step #5: Get ELB Hosted Zone Id - if appliable
209      */
210     if (!ctx.singleInstance) {
211       String loadBalancerName =
212           parentMojo
213               .getService()
214               .describeEnvironmentResources(new DescribeEnvironmentResourcesRequest().withEnvironmentId(ctx.getCurEnv().getEnvironmentId()))
215               .getEnvironmentResources()
216               .getLoadBalancers()
217               .get(0)
218               .getName();
219 
220       DescribeLoadBalancersRequest req = new DescribeLoadBalancersRequest(asList(loadBalancerName));
221 
222       List<LoadBalancerDescription> loadBalancers = elb.describeLoadBalancers(req).getLoadBalancerDescriptions();
223 
224       Validate.isTrue(1 == loadBalancers.size(), "Unexpected number of Load Balancers returned");
225 
226       ctx.elbHostedZoneId = loadBalancers.get(0).getCanonicalHostedZoneNameID();
227 
228       if (isInfoEnabled()) {
229         info(format("Using ELB Canonical Hosted Zone Name Id %s", ctx.elbHostedZoneId));
230       }
231     }
232 
233     /**
234      * Step #6: Apply Change Batch on Each Domain
235      */
236     for (Map.Entry<String, String> recordEntry : recordsToAssign.entrySet()) {
237       assignDomain(ctx, recordEntry.getKey(), recordEntry.getValue());
238     }
239 
240     return null;
241   }
242 
243   protected void assignDomain(BindDomainsContext ctx, String record, String zoneId) {
244     ChangeBatch changeBatch = new ChangeBatch();
245 
246     changeBatch.setComment(format("Updated for env %s", ctx.getCurEnv().getCNAME()));
247 
248     /**
249      * Look for Existing Resource Record Sets
250      */
251     {
252       ResourceRecordSet resourceRecordSet = null;
253 
254       ListResourceRecordSetsResult listResourceRecordSets = r53.listResourceRecordSets(new ListResourceRecordSetsRequest(zoneId));
255 
256       for (ResourceRecordSet rrs : listResourceRecordSets.getResourceRecordSets()) {
257         if (!rrs.getName().equals(record)) {
258           continue;
259         }
260 
261         boolean matchesTypes = "A".equals(rrs.getType()) || "CNAME".equals(rrs.getType());
262 
263         if (!matchesTypes) {
264           continue;
265         }
266 
267         if (isInfoEnabled()) {
268           info("Excluding resourceRecordSet %s for domain %s", rrs, record);
269         }
270 
271         changeBatch.getChanges().add(new Change(ChangeAction.DELETE, rrs));
272       }
273     }
274 
275     /**
276      * Then Add Ours
277      */
278     ResourceRecordSet resourceRecordSet = new ResourceRecordSet();
279 
280     resourceRecordSet.setName(record);
281     resourceRecordSet.setType(RRType.A);
282 
283     if (ctx.singleInstance) {
284       final String address = ctx.getCurEnv().getEndpointURL();
285       ResourceRecord resourceRecord = new ResourceRecord(address);
286 
287       resourceRecordSet.setTTL(60L);
288       resourceRecordSet.setResourceRecords(asList(resourceRecord));
289 
290       if (isInfoEnabled()) {
291         info("Adding resourceRecordSet %s for domain %s mapped to %s", resourceRecordSet, record, address);
292       }
293     } else {
294       AliasTarget aliasTarget = new AliasTarget();
295 
296       aliasTarget.setHostedZoneId(ctx.getElbHostedZoneId());
297       aliasTarget.setDNSName(ctx.getCurEnv().getEndpointURL());
298 
299       resourceRecordSet.setAliasTarget(aliasTarget);
300 
301       if (isInfoEnabled()) {
302         info("Adding resourceRecordSet %s for domain %s mapped to %s", resourceRecordSet, record, aliasTarget.getDNSName());
303       }
304     }
305 
306     changeBatch.getChanges().add(new Change(ChangeAction.CREATE, resourceRecordSet));
307 
308     if (isInfoEnabled()) {
309       info("Changes to be sent: %s", changeBatch.getChanges());
310     }
311 
312     ChangeResourceRecordSetsRequest req = new ChangeResourceRecordSetsRequest(zoneId, changeBatch);
313 
314     r53.changeResourceRecordSets(req);
315   }
316 
317   String formatDomain(String d) {
318     return strip(d, ".").concat(".");
319   }
320 }