Coverage Report - com.jcabi.beanstalk.maven.plugin.Application
 
Classes in this File Line Coverage Branch Coverage Complexity
Application
50%
42/84
20%
14/68
3.154
Application$AjcClosure1
100%
1/1
N/A
3.154
Application$AjcClosure3
0%
0/1
N/A
3.154
Application$AjcClosure5
0%
0/1
N/A
3.154
Application$AjcClosure7
0%
0/1
N/A
3.154
Application$AjcClosure9
100%
1/1
N/A
3.154
 
 1  2
 /**
 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  0
 @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  1
         @NotNull final String app) {
 79  1
         this.client = clnt;
 80  1
         this.name = app;
 81  1
         Logger.info(
 82  
             Application.class,
 83  
             "Working with application '%s'",
 84  
             this.name
 85  
         );
 86  1
     }
 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  2
         for (final Environment env : this.environments()) {
 94  1
             if (env.primary() && env.green() && !wipe) {
 95  0
                 Logger.info(
 96  
                     this,
 97  
                     "Environment '%s' is primary and green",
 98  
                     env
 99  
                 );
 100  0
                 continue;
 101  
             }
 102  1
             if (env.terminated()) {
 103  0
                 continue;
 104  
             }
 105  1
             if (wipe) {
 106  0
                 Logger.info(
 107  
                     this,
 108  
                     // @checkstyle LineLength (1 line)
 109  
                     "Wiping out environment '%s' as required by configuration...",
 110  
                     env
 111  
                 );
 112  
             } else {
 113  1
                 Logger.info(
 114  
                     this,
 115  
                     "Environment '%s' is not primary+green, terminating...",
 116  
                     env
 117  
                 );
 118  
             }
 119  1
             env.terminate();
 120  1
         }
 121  1
     }
 122  
 
 123  
     /**
 124  
      * {@inheritDoc}
 125  
      */
 126  
     @Override
 127  
     public String toString() {
 128  0
         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  0
         Environment primary = null;
 137  0
         for (final Environment env : this.environments()) {
 138  0
             if (env.primary()) {
 139  0
                 primary = env;
 140  0
                 break;
 141  
             }
 142  0
         }
 143  0
         if (primary == null) {
 144  0
             throw new DeploymentException(
 145  
                 String.format(
 146  
                     "Application '%s' doesn't have a primary env",
 147  
                     this.name
 148  
                 )
 149  
             );
 150  
         }
 151  0
         return primary;
 152  
     }
 153  
 
 154  
     /**
 155  
      * This application has a primary environment?
 156  
      * @return TRUE if it exists
 157  
      */
 158  
     public boolean hasPrimary() {
 159  0
         boolean has = false;
 160  0
         for (final Environment env : this.environments()) {
 161  0
             if (env.primary() && env.green()) {
 162  0
                 has = true;
 163  0
                 break;
 164  
             }
 165  0
         }
 166  0
         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  0
         final Environment primary = this.primary();
 175  0
         this.client.swapEnvironmentCNAMEs(
 176  
             new SwapEnvironmentCNAMEsRequest()
 177  
                 .withDestinationEnvironmentName(primary.name())
 178  
                 .withSourceEnvironmentName(candidate.name())
 179  
         );
 180  0
         Logger.info(
 181  
             this,
 182  
             "Environment '%s' swapped CNAME with '%s'",
 183  
             candidate.name(), primary.name()
 184  
         );
 185  0
         if (candidate.stable() && !candidate.primary()) {
 186  0
             throw new DeploymentException(
 187  
                 String.format(
 188  
                     "Failed to swap, '%s' didn't become a primary env",
 189  
                     candidate
 190  
                 )
 191  
             );
 192  
         }
 193  0
         if (primary.stable() && primary.primary()) {
 194  0
             throw new DeploymentException(
 195  
                 String.format(
 196  
                     "Failed to swap, '%s' is still a primary env",
 197  
                     primary
 198  
                 )
 199  
             );
 200  
         }
 201  0
         primary.terminate();
 202  0
     }
 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  2
         final CreateEnvironmentRequest request = this.suggest();
 213  1
         Logger.info(
 214  
             this,
 215  
             "Suggested candidate environment name is '%s' with '%s' CNAME",
 216  
             request.getEnvironmentName(),
 217  
             request.getCNAMEPrefix()
 218  
         );
 219  1
         final CreateEnvironmentResult res = this.client.createEnvironment(
 220  
             request
 221  
                 .withApplicationName(this.name)
 222  
                 .withVersionLabel(version.label())
 223  
                 .withTemplateName(template)
 224  
         );
 225  1
         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  1
         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  2
         final DescribeEnvironmentsResult res = this.client.describeEnvironments(
 243  
             new DescribeEnvironmentsRequest().withApplicationName(this.name)
 244  
         );
 245  2
         final Collection<Environment> envs = new LinkedList<Environment>();
 246  2
         for (final EnvironmentDescription desc : res.getEnvironments()) {
 247  2
             envs.add(new Environment(this.client, desc.getEnvironmentId()));
 248  2
         }
 249  2
         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  1
         final CreateEnvironmentRequest request = new CreateEnvironmentRequest();
 259  
         while (true) {
 260  1
             if (!this.occupied(this.name)) {
 261  1
                 request.withCNAMEPrefix(this.name);
 262  1
                 break;
 263  
             }
 264  0
             if (this.hasPrimary()) {
 265  0
                 request.withCNAMEPrefix(this.makeup());
 266  0
                 break;
 267  
             }
 268  0
             Logger.info(this, "Waiting for '%s' CNAME", this.name);
 269  
         }
 270  
         while (true) {
 271  1
             final String ename = this.random();
 272  1
             if (!this.exists(ename)) {
 273  1
                 request.withEnvironmentName(ename).withDescription(ename);
 274  1
                 Logger.info(this, "Using '%s' as env name", ename);
 275  1
                 break;
 276  
             }
 277  0
         }
 278  1
         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  0
             cname = this.random();
 289  0
             Logger.info(this, "Trying '%s' CNAME", cname);
 290  0
         } while (this.occupied(cname));
 291  0
         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  1
         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  1
         boolean exists = false;
 312  1
         for (final Environment env : this.environments()) {
 313  1
             if (env.name().equals(ename)) {
 314  0
                 exists = true;
 315  0
                 break;
 316  
             }
 317  1
         }
 318  1
         return exists;
 319  
     }
 320  
 
 321  
     /**
 322  
      * Generate random name.
 323  
      * @return Random name
 324  
      */
 325  
     private String random() {
 326  1
         return String.format(
 327  
             "%s-e%03d",
 328  
             this.name,
 329  
             Tv.HUNDRED + new Random().nextInt(Tv.NINE * Tv.HUNDRED)
 330  
         );
 331  
     }
 332  
 
 333  
 }