View Javadoc
1   /**
2    * Copyright (c) 2012-2014, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
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   * EBT application.
51   *
52   * @author Yegor Bugayenko (yegor@tpc2.com)
53   * @version $Id$
54   * @since 0.3
55   * @checkstyle ClassDataAbstractionCoupling (500 lines)
56   */
57  @EqualsAndHashCode(of = { "client", "name" })
58  @SuppressWarnings("PMD.TooManyMethods")
59  @Loggable(Loggable.DEBUG)
60  final class Application {
61  
62      /**
63       * AWS beanstalk client.
64       */
65      private final transient AWSElasticBeanstalk client;
66  
67      /**
68       * Application name.
69       */
70      private final transient String name;
71  
72      /**
73       * Public ctor.
74       * @param clnt The client
75       * @param app Application name
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       * Clean it up beforehand.
90       * @param wipe Kill all existing environments no matter what?
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                     // @checkstyle LineLength (1 line)
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      * {@inheritDoc}
125      */
126     @Override
127     public String toString() {
128         return this.name;
129     }
130 
131     /**
132      * Get primary environment or throws a runtime exception if it is absent.
133      * @return Primary environment
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      * This application has a primary environment?
156      * @return TRUE if it exists
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      * Activate candidate environment by swap of CNAMEs.
171      * @param candidate The candidate to make a primary environment
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      * Create candidate environment.
206      * @param version Version to deploy
207      * @param template EBT configuration template
208      * @return The environment
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             // @checkstyle LineLength (1 line)
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      * Get all environments in this app.
238      * @return Collection of envs
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      * Suggest new candidate environment CNAME (and at the same time it will
254      * be used as a name of environment).
255      * @return The environment create request with data inside
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      * Make up a nice CNAME in this application.
283      * @return The CNAME, suggested and not occupied
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      * This CNAME is occupied?
296      * @param cname The CNAME to check
297      * @return TRUE if it's occupied
298      */
299     private boolean occupied(final String cname) {
300         return !this.client.checkDNSAvailability(
301             new CheckDNSAvailabilityRequest(cname)
302         ).getAvailable();
303     }
304 
305     /**
306      * This environment exists?
307      * @param ename The name of environment to check
308      * @return TRUE if it exists
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      * Generate random name.
323      * @return Random name
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 }