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.ConfigurationOptionSetting;
34  import com.amazonaws.services.elasticbeanstalk.model.ConfigurationSettingsDescription;
35  import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsRequest;
36  import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationSettingsResult;
37  import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentsRequest;
38  import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentsResult;
39  import com.amazonaws.services.elasticbeanstalk.model.DescribeEventsRequest;
40  import com.amazonaws.services.elasticbeanstalk.model.DescribeEventsResult;
41  import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription;
42  import com.amazonaws.services.elasticbeanstalk.model.EnvironmentInfoDescription;
43  import com.amazonaws.services.elasticbeanstalk.model.EnvironmentInfoType;
44  import com.amazonaws.services.elasticbeanstalk.model.EventDescription;
45  import com.amazonaws.services.elasticbeanstalk.model.RequestEnvironmentInfoRequest;
46  import com.amazonaws.services.elasticbeanstalk.model.RetrieveEnvironmentInfoRequest;
47  import com.amazonaws.services.elasticbeanstalk.model.TerminateEnvironmentRequest;
48  import com.amazonaws.services.elasticbeanstalk.model.TerminateEnvironmentResult;
49  import com.amazonaws.services.elasticbeanstalk.model.UpdateEnvironmentRequest;
50  import com.amazonaws.services.elasticbeanstalk.model.UpdateEnvironmentResult;
51  import com.jcabi.aspects.Loggable;
52  import com.jcabi.aspects.Tv;
53  import com.jcabi.log.Logger;
54  import java.io.IOException;
55  import java.net.URL;
56  import java.util.Collection;
57  import java.util.LinkedList;
58  import java.util.List;
59  import java.util.concurrent.TimeUnit;
60  import javax.validation.constraints.NotNull;
61  import lombok.EqualsAndHashCode;
62  import org.apache.commons.io.IOUtils;
63  
64  /**
65   * EBT environment.
66   *
67   * @author Yegor Bugayenko (yegor@tpc2.com)
68   * @version $Id$
69   * @since 0.3
70   * @checkstyle ClassDataAbstractionCoupling (500 lines)
71   */
72  @EqualsAndHashCode(of = { "client", "eid" })
73  @SuppressWarnings({ "PMD.TooManyMethods", "PMD.ExcessiveImports" })
74  @Loggable(Loggable.DEBUG)
75  final class Environment {
76  
77      /**
78       * For how long we can wait until env reaches certain status.
79       */
80      private static final long DELAY_MS = TimeUnit.MINUTES.toMillis(Tv.THIRTY);
81  
82      /**
83       * AWS beanstalk client.
84       */
85      private final transient AWSElasticBeanstalk client;
86  
87      /**
88       * Environment ID.
89       */
90      private final transient String eid;
91  
92      /**
93       * Public ctor.
94       * @param clnt The client
95       * @param idnt Environment ID
96       */
97      protected Environment(@NotNull final AWSElasticBeanstalk clnt,
98          @NotNull final String idnt) {
99          this.client = clnt;
100         this.eid = idnt;
101         final EnvironmentDescription desc = this.description();
102         final String template = desc.getTemplateName();
103         if (template != null) {
104             final DescribeConfigurationSettingsResult res =
105                 this.client.describeConfigurationSettings(
106                     new DescribeConfigurationSettingsRequest()
107                         .withApplicationName(desc.getApplicationName())
108                         .withTemplateName(template)
109                 );
110             for (final ConfigurationSettingsDescription config
111                 : res.getConfigurationSettings()) {
112                 Logger.debug(
113                     Environment.class,
114                     "Environment '%s/%s/%s' settings:",
115                     config.getApplicationName(), config.getEnvironmentName()
116                 );
117                 for (final ConfigurationOptionSetting opt
118                     : config.getOptionSettings()) {
119                     Logger.debug(
120                         Environment.class,
121                         "  %s/%s: %s",
122                         opt.getNamespace(), opt.getOptionName(), opt.getValue()
123                     );
124                 }
125             }
126         }
127     }
128 
129     /**
130      * {@inheritDoc}
131      */
132     @Override
133     public String toString() {
134         final EnvironmentDescription desc = this.description();
135         return String.format(
136             "%s/%s/%s",
137             desc.getEnvironmentName(), desc.getEnvironmentId(), desc.getCNAME()
138         );
139     }
140 
141     /**
142      * Is it primary environment in the application?
143      * @return TRUE if this environment is attached to the main CNAME
144      */
145     public boolean primary() {
146         final EnvironmentDescription desc = this.description();
147         final String prefix = String.format("%s.", desc.getApplicationName());
148         final boolean primary = this.stable()
149             && desc.getCNAME().startsWith(prefix);
150         if (primary) {
151             Logger.info(
152                 this,
153                 "Environment '%s' considered primary", this
154             );
155         } else {
156             Logger.info(
157                 this,
158                 // @checkstyle LineLength (1 line)
159                 "Environment '%s' considered secondary since its CNAME doesn't start with '%s'",
160                 this, prefix
161             );
162         }
163         return primary;
164     }
165 
166     /**
167      * Get environment name.
168      * @return Name of it
169      */
170     public String name() {
171         return this.description().getEnvironmentName();
172     }
173 
174     /**
175      * Environment is in Green health?
176      * @return TRUE if environment is in Green health
177      */
178     public boolean green() {
179         return this.stable() && "Green".equals(this.description().getHealth());
180     }
181 
182     /**
183      * Wait for stable state, and return TRUE if achieved or FALSE if not.
184      * @return TRUE if environment is stable
185      */
186     public boolean stable() {
187         return this.until(
188             new Environment.Barrier() {
189                 @Override
190                 public String message() {
191                     return "stable state";
192                 }
193                 @Override
194                 public boolean allow(final EnvironmentDescription desc) {
195                     return !desc.getStatus().matches(".*ing$");
196                 }
197             }
198         );
199     }
200 
201     /**
202      * Is it terminated?
203      * @return Yes or no
204      */
205     public boolean terminated() {
206         return this.stable()
207             && "Terminated".equals(this.description().getStatus());
208     }
209 
210     /**
211      * Terminate environment.
212      */
213     public void terminate() {
214         if (!this.stable()) {
215             throw new DeploymentException(
216                 String.format(
217                     "env '%s' is not stable, can't terminate",
218                     this.eid
219                 )
220             );
221         }
222         if (!this.terminated()) {
223             final TerminateEnvironmentResult res =
224                 this.client.terminateEnvironment(
225                     new TerminateEnvironmentRequest()
226                         .withEnvironmentId(this.eid)
227                         .withTerminateResources(true)
228                 );
229             Logger.info(
230                 this,
231                 "Environment '%s/%s/%s' is terminated (label:'%s', status:%s)",
232                 res.getApplicationName(), res.getEnvironmentName(),
233                 res.getEnvironmentId(),
234                 res.getVersionLabel(), res.getStatus()
235             );
236         }
237     }
238 
239     /**
240      * Get latest events.
241      * @return Collection of events
242      */
243     public String[] events() {
244         if (!this.stable()) {
245             throw new DeploymentException(
246                 String.format(
247                     "env '%s' is not stable, can't get list of events",
248                     this.eid
249                 )
250             );
251         }
252         final DescribeEventsResult res = this.client.describeEvents(
253             new DescribeEventsRequest().withEnvironmentId(this.eid)
254         );
255         final Collection<String> events = new LinkedList<String>();
256         for (final EventDescription desc : res.getEvents()) {
257             events.add(
258                 String.format("[%s]: %s", desc.getSeverity(), desc.getMessage())
259             );
260         }
261         return events.toArray(new String[events.size()]);
262     }
263 
264     /**
265      * Tail log.
266      * @return Full text of tail log from the environment
267      */
268     public String tail() {
269         if (!this.stable()) {
270             throw new DeploymentException(
271                 String.format(
272                     "env '%s' is not stable, can't get TAIL report",
273                     this.eid
274                 )
275             );
276         }
277         if (this.terminated()) {
278             throw new DeploymentException(
279                 String.format(
280                     "env '%s' is terminated, can't get TAIL report",
281                     this.eid
282                 )
283             );
284         }
285         this.client.requestEnvironmentInfo(
286             new RequestEnvironmentInfoRequest()
287                 .withEnvironmentId(this.eid)
288                 .withInfoType(EnvironmentInfoType.Tail)
289         );
290         final RetrieveEnvironmentInfoRequest req =
291             new RetrieveEnvironmentInfoRequest()
292                 .withEnvironmentId(this.eid)
293                 .withInfoType(EnvironmentInfoType.Tail);
294         List<EnvironmentInfoDescription> infos;
295         final long start = System.currentTimeMillis();
296         do {
297             if (System.currentTimeMillis() - start > Environment.DELAY_MS) {
298                 throw new DeploymentException(
299                     String.format(
300                         "env '%s' doesn't report its TAIL, time out",
301                         this.eid
302                     )
303                 );
304             }
305             Logger.info(
306                 this,
307                 "Waiting for TAIL report of %s",
308                 this.eid
309             );
310             infos = this.client
311                 .retrieveEnvironmentInfo(req)
312                 .getEnvironmentInfo();
313         } while (infos.isEmpty());
314         final EnvironmentInfoDescription desc = infos.get(0);
315         try {
316             return IOUtils.toString(new URL(desc.getMessage()).openStream());
317         } catch (final IOException ex) {
318             throw new IllegalStateException(ex);
319         }
320     }
321 
322     /**
323      * Update this environment with a new version.
324      * @param version The version to update to
325      */
326     public void update(final Version version) {
327         final UpdateEnvironmentResult res = this.client.updateEnvironment(
328             new UpdateEnvironmentRequest()
329                 .withEnvironmentId(this.eid)
330                 .withVersionLabel(version.label())
331         );
332         Logger.info(
333             this,
334             "Environment '%s' updated to '%s'",
335             res.getEnvironmentId(), res.getVersionLabel()
336         );
337     }
338 
339     /**
340      * Get environment description of this.
341      * @return The description
342      */
343     private EnvironmentDescription description() {
344         final DescribeEnvironmentsResult res = this.client.describeEnvironments(
345             new DescribeEnvironmentsRequest()
346                 .withEnvironmentIds(this.eid)
347         );
348         if (res.getEnvironments().isEmpty()) {
349             throw new DeploymentException(
350                 String.format("environment '%s' not found", this.eid)
351             );
352         }
353         final EnvironmentDescription desc = res.getEnvironments().get(0);
354         Logger.debug(
355             this,
356             // @checkstyle LineLength (1 line)
357             "ID=%s, env=%s, app=%s, CNAME=%s, label=%s, template=%s, status=%s, health=%s",
358             desc.getEnvironmentId(), desc.getEnvironmentName(),
359             desc.getApplicationName(), desc.getCNAME(),
360             desc.getVersionLabel(), desc.getTemplateName(), desc.getStatus(),
361             desc.getHealth()
362         );
363         return desc;
364     }
365 
366     /**
367      * Wait for the barrier to pass.
368      * @param barrier The barrier
369      * @return TRUE if passed, FALSE if timeout
370      */
371     private boolean until(final Environment.Barrier barrier) {
372         boolean passed = false;
373         final long start = System.currentTimeMillis();
374         while (true) {
375             final EnvironmentDescription desc = this.description();
376             if (barrier.allow(desc)) {
377                 passed = true;
378                 Logger.info(
379                     this,
380                     "Environment '%s/%s/%s': health=%s, status=%s",
381                     desc.getApplicationName(), desc.getEnvironmentName(),
382                     desc.getEnvironmentId(), desc.getHealth(),
383                     desc.getStatus()
384                 );
385                 break;
386             }
387             Logger.info(
388                 this,
389                 // @checkstyle LineLength (1 line)
390                 "Environment '%s/%s/%s': health=%s, status=%s (waiting for %s, %[ms]s)",
391                 desc.getApplicationName(), desc.getEnvironmentName(),
392                 desc.getEnvironmentId(), desc.getHealth(),
393                 desc.getStatus(), barrier.message(),
394                 System.currentTimeMillis() - start
395             );
396             if (System.currentTimeMillis() - start > Environment.DELAY_MS) {
397                 Logger.warn(
398                     this,
399                     "Environment failed to reach '%s' after %[ms]s",
400                     barrier.message(), System.currentTimeMillis() - start
401                 );
402                 break;
403             }
404             try {
405                 TimeUnit.MINUTES.sleep(1);
406             } catch (final InterruptedException ex) {
407                 Thread.currentThread().interrupt();
408                 throw new DeploymentException(ex);
409             }
410         }
411         return passed;
412     }
413 
414     /**
415      * Barrier before the next operation.
416      */
417     private interface Barrier {
418         /**
419          * Can we continue?
420          * @param desc Description of environment
421          * @return TRUE if we can continue, FALSE if extra cycle of waiting
422          *  is required
423          */
424         boolean allow(EnvironmentDescription desc);
425         /**
426          * What are we waiting for?
427          * @return Message to show in log
428          */
429         String message();
430     }
431 
432 }