1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
71
72
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
117
118 {
119 for (String domain : ctx.getDomains()) {
120 String key = formatDomain(domain);
121 String value = null;
122
123
124
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
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
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
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
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
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
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 }