1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package com.jcabi.beanstalk.maven.plugin;
31
32 import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalk;
33 import com.amazonaws.services.elasticbeanstalk.model.CheckDNSAvailabilityRequest;
34 import com.amazonaws.services.elasticbeanstalk.model.CreateEnvironmentRequest;
35 import com.amazonaws.services.elasticbeanstalk.model.CreateEnvironmentResult;
36 import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentsRequest;
37 import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentsResult;
38 import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
39 import com.amazonaws.services.elasticbeanstalk.model.SwapEnvironmentCNAMEsRequest;
40 import com.jcabi.aspects.Loggable;
41 import com.jcabi.aspects.Tv;
42 import com.jcabi.log.Logger;
43 import java.util.Collection;
44 import java.util.LinkedList;
45 import java.util.Random;
46 import javax.validation.constraints.NotNull;
47 import lombok.EqualsAndHashCode;
48
49
50
51
52
53
54
55
56
57 @EqualsAndHashCode(of = { "client", "name" })
58 @SuppressWarnings("PMD.TooManyMethods")
59 @Loggable(Loggable.DEBUG)
60 final class Application {
61
62
63
64
65 private final transient AWSElasticBeanstalk client;
66
67
68
69
70 private final transient String name;
71
72
73
74
75
76
77 protected Application(@NotNull final AWSElasticBeanstalk clnt,
78 @NotNull final String app) {
79 this.client = clnt;
80 this.name = app;
81 Logger.info(
82 Application.class,
83 "Working with application '%s'",
84 this.name
85 );
86 }
87
88
89
90
91
92 public void clean(final boolean wipe) {
93 for (final Environment env : this.environments()) {
94 if (env.primary() && env.green() && !wipe) {
95 Logger.info(
96 this,
97 "Environment '%s' is primary and green",
98 env
99 );
100 continue;
101 }
102 if (env.terminated()) {
103 continue;
104 }
105 if (wipe) {
106 Logger.info(
107 this,
108
109 "Wiping out environment '%s' as required by configuration...",
110 env
111 );
112 } else {
113 Logger.info(
114 this,
115 "Environment '%s' is not primary+green, terminating...",
116 env
117 );
118 }
119 env.terminate();
120 }
121 }
122
123
124
125
126 @Override
127 public String toString() {
128 return this.name;
129 }
130
131
132
133
134
135 public Environment primary() {
136 Environment primary = null;
137 for (final Environment env : this.environments()) {
138 if (env.primary()) {
139 primary = env;
140 break;
141 }
142 }
143 if (primary == null) {
144 throw new DeploymentException(
145 String.format(
146 "Application '%s' doesn't have a primary env",
147 this.name
148 )
149 );
150 }
151 return primary;
152 }
153
154
155
156
157
158 public boolean hasPrimary() {
159 boolean has = false;
160 for (final Environment env : this.environments()) {
161 if (env.primary() && env.green()) {
162 has = true;
163 break;
164 }
165 }
166 return has;
167 }
168
169
170
171
172
173 public void swap(@NotNull final Environment candidate) {
174 final Environment primary = this.primary();
175 this.client.swapEnvironmentCNAMEs(
176 new SwapEnvironmentCNAMEsRequest()
177 .withDestinationEnvironmentName(primary.name())
178 .withSourceEnvironmentName(candidate.name())
179 );
180 Logger.info(
181 this,
182 "Environment '%s' swapped CNAME with '%s'",
183 candidate.name(), primary.name()
184 );
185 if (candidate.stable() && !candidate.primary()) {
186 throw new DeploymentException(
187 String.format(
188 "Failed to swap, '%s' didn't become a primary env",
189 candidate
190 )
191 );
192 }
193 if (primary.stable() && primary.primary()) {
194 throw new DeploymentException(
195 String.format(
196 "Failed to swap, '%s' is still a primary env",
197 primary
198 )
199 );
200 }
201 primary.terminate();
202 }
203
204
205
206
207
208
209
210 public Environment candidate(@NotNull final Version version,
211 @NotNull final String template) {
212 final CreateEnvironmentRequest request = this.suggest();
213 Logger.info(
214 this,
215 "Suggested candidate environment name is '%s' with '%s' CNAME",
216 request.getEnvironmentName(),
217 request.getCNAMEPrefix()
218 );
219 final CreateEnvironmentResult res = this.client.createEnvironment(
220 request
221 .withApplicationName(this.name)
222 .withVersionLabel(version.label())
223 .withTemplateName(template)
224 );
225 Logger.info(
226 this,
227
228 "Candidate environment '%s/%s/%s' created at CNAME '%s' (status:%s, health:%s)",
229 res.getApplicationName(), res.getEnvironmentName(),
230 res.getEnvironmentId(), res.getCNAME(),
231 res.getStatus(), res.getHealth()
232 );
233 return new Environment(this.client, res.getEnvironmentId());
234 }
235
236
237
238
239
240 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
241 private Collection<Environment> environments() {
242 final DescribeEnvironmentsResult res = this.client.describeEnvironments(
243 new DescribeEnvironmentsRequest().withApplicationName(this.name)
244 );
245 final Collection<Environment> envs = new LinkedList<Environment>();
246 for (final EnvironmentDescription desc : res.getEnvironments()) {
247 envs.add(new Environment(this.client, desc.getEnvironmentId()));
248 }
249 return envs;
250 }
251
252
253
254
255
256
257 private CreateEnvironmentRequest suggest() {
258 final CreateEnvironmentRequest request = new CreateEnvironmentRequest();
259 while (true) {
260 if (!this.occupied(this.name)) {
261 request.withCNAMEPrefix(this.name);
262 break;
263 }
264 if (this.hasPrimary()) {
265 request.withCNAMEPrefix(this.makeup());
266 break;
267 }
268 Logger.info(this, "Waiting for '%s' CNAME", this.name);
269 }
270 while (true) {
271 final String ename = this.random();
272 if (!this.exists(ename)) {
273 request.withEnvironmentName(ename).withDescription(ename);
274 Logger.info(this, "Using '%s' as env name", ename);
275 break;
276 }
277 }
278 return request;
279 }
280
281
282
283
284
285 private String makeup() {
286 String cname;
287 do {
288 cname = this.random();
289 Logger.info(this, "Trying '%s' CNAME", cname);
290 } while (this.occupied(cname));
291 return cname;
292 }
293
294
295
296
297
298
299 private boolean occupied(final String cname) {
300 return !this.client.checkDNSAvailability(
301 new CheckDNSAvailabilityRequest(cname)
302 ).getAvailable();
303 }
304
305
306
307
308
309
310 private boolean exists(final String ename) {
311 boolean exists = false;
312 for (final Environment env : this.environments()) {
313 if (env.name().equals(ename)) {
314 exists = true;
315 break;
316 }
317 }
318 return exists;
319 }
320
321
322
323
324
325 private String random() {
326 return String.format(
327 "%s-e%03d",
328 this.name,
329 Tv.HUNDRED + new Random().nextInt(Tv.NINE * Tv.HUNDRED)
330 );
331 }
332
333 }