elastic_beans 0.10.0.alpha1 → 0.10.0.alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +20 -0
- data/elastic_beans.gemspec +1 -1
- data/lib/elastic_beans/application.rb +55 -31
- data/lib/elastic_beans/aws/cloudformation_stack.rb +3 -0
- data/lib/elastic_beans/cli.rb +15 -0
- data/lib/elastic_beans/command/exec.rb +3 -2
- data/lib/elastic_beans/command/kill.rb +38 -0
- data/lib/elastic_beans/command/ps.rb +5 -13
- data/lib/elastic_beans/command.rb +1 -0
- data/lib/elastic_beans/env_vars.rb +2 -15
- data/lib/elastic_beans/error/access_denied.rb +38 -0
- data/lib/elastic_beans/exec/sqs_consumer.rb +56 -1
- data/lib/elastic_beans/version.rb +1 -1
- metadata +7 -5
- data/circle.yml +0 -24
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 9c681be8f1584e92ac3326f36602a6f006739fae
         | 
| 4 | 
            +
              data.tar.gz: 2d7efd88e9401d2c8ff327f15992de92ba94323d
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 03076cb44db365dcf4d20fdfe45aad0a1d845b9ad346fdb8b6074be7073045c4f94fe411e637aafd965ddce9bf54dbd03cde18f1e675d89ce75fbde5be9e462d
         | 
| 7 | 
            +
              data.tar.gz: fefcc5407eb3a0f5a19e317b85fb1f6c2aa0453d5ad1513648a5260fb3e01b9b55e17489641e2a9e0f63b8da6544f15f6825c16ba07bc52eab1aa2c1e77fc8bc
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            version: 2
         | 
| 2 | 
            +
            jobs:
         | 
| 3 | 
            +
              build:
         | 
| 4 | 
            +
                docker:
         | 
| 5 | 
            +
                - image: ruby:latest
         | 
| 6 | 
            +
                working_directory: /elastic_beans
         | 
| 7 | 
            +
                steps:
         | 
| 8 | 
            +
                - checkout
         | 
| 9 | 
            +
                - run:
         | 
| 10 | 
            +
                    name: Configure git
         | 
| 11 | 
            +
                    command: |
         | 
| 12 | 
            +
                      git config --global user.name "CircleCI"
         | 
| 13 | 
            +
                      git config --global user.email "ops-ro@onemedical.com"
         | 
| 14 | 
            +
                - run: bundle install --jobs=4 --retry=3
         | 
| 15 | 
            +
                - run:
         | 
| 16 | 
            +
                    command: bundle exec rake
         | 
| 17 | 
            +
                    environment:
         | 
| 18 | 
            +
                      SPEC_OPTS: --format progress --format RspecJunitFormatter -o tmp/test_results/rspec/junit.xml
         | 
| 19 | 
            +
                - store_test_results:
         | 
| 20 | 
            +
                    path: tmp/test_results/
         | 
    
        data/elastic_beans.gemspec
    CHANGED
    
    | @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| | |
| 25 25 | 
             
              spec.add_dependency "ruby-progressbar", "~> 1.2"
         | 
| 26 26 | 
             
              spec.add_dependency "rubyzip", "~> 1.2"
         | 
| 27 27 | 
             
              spec.add_dependency "thor", "~> 0.19.0"
         | 
| 28 | 
            -
              spec.add_dependency "tty-table", "~> 0. | 
| 28 | 
            +
              spec.add_dependency "tty-table", "~> 0.8.0"
         | 
| 29 29 |  | 
| 30 30 | 
             
              spec.add_development_dependency "bundler", "~> 1.12"
         | 
| 31 31 | 
             
              spec.add_development_dependency "rake", "~> 10.0"
         | 
| @@ -3,6 +3,7 @@ require "timeout" | |
| 3 3 | 
             
            require "aws-sdk"
         | 
| 4 4 | 
             
            require "elastic_beans/aws/cloudformation_stack"
         | 
| 5 5 | 
             
            require "elastic_beans/error"
         | 
| 6 | 
            +
            require "elastic_beans/error/access_denied"
         | 
| 6 7 |  | 
| 7 8 | 
             
            module ElasticBeans
         | 
| 8 9 | 
             
              # An Elastic Beanstalk application which should exist.
         | 
| @@ -42,7 +43,7 @@ module ElasticBeans | |
| 42 43 | 
             
                  response = elastic_beanstalk.describe_applications(application_names: [name])
         | 
| 43 44 | 
             
                  application = response.applications[0]
         | 
| 44 45 | 
             
                  unless application
         | 
| 45 | 
            -
                    raise MissingApplicationError
         | 
| 46 | 
            +
                    raise MissingApplicationError.new(application: self)
         | 
| 46 47 | 
             
                  end
         | 
| 47 48 |  | 
| 48 49 | 
             
                  templates = application.configuration_templates
         | 
| @@ -61,7 +62,7 @@ module ElasticBeans | |
| 61 62 | 
             
                # Returns the ElasticBeans::EnvVars for this application.
         | 
| 62 63 | 
             
                def env_vars
         | 
| 63 64 | 
             
                  unless exists?
         | 
| 64 | 
            -
                    raise MissingApplicationError
         | 
| 65 | 
            +
                    raise MissingApplicationError.new(application: self)
         | 
| 65 66 | 
             
                  end
         | 
| 66 67 |  | 
| 67 68 | 
             
                  EnvVars.new(application: self, s3: s3)
         | 
| @@ -96,6 +97,8 @@ module ElasticBeans | |
| 96 97 | 
             
                    raise MissingBucketError
         | 
| 97 98 | 
             
                  end
         | 
| 98 99 | 
             
                  @bucket_name = bucket.name
         | 
| 100 | 
            +
                rescue ::Aws::S3::Errors::AccessDenied
         | 
| 101 | 
            +
                  raise AccessDeniedS3Error.new
         | 
| 99 102 | 
             
                end
         | 
| 100 103 |  | 
| 101 104 | 
             
                # Enqueues a one-off Exec::Command to be run on the application's +exec+ environment.
         | 
| @@ -112,45 +115,31 @@ module ElasticBeans | |
| 112 115 | 
             
                  end
         | 
| 113 116 | 
             
                  command.metadata[:bucket] = bucket_name
         | 
| 114 117 | 
             
                  command.metadata[:key] = "#{command_key_prefix}#{command.id}.json"
         | 
| 118 | 
            +
                  s3.put_object(
         | 
| 119 | 
            +
                    bucket: bucket_name,
         | 
| 120 | 
            +
                    key: command.metadata[:key],
         | 
| 121 | 
            +
                    body: command.to_json,
         | 
| 122 | 
            +
                  )
         | 
| 115 123 |  | 
| 116 124 | 
             
                  sqs.send_message(
         | 
| 117 125 | 
             
                    queue_url: exec_queue_url,
         | 
| 118 126 | 
             
                    message_body: command.to_json,
         | 
| 119 127 | 
             
                  )
         | 
| 128 | 
            +
                rescue ::Aws::S3::Errors::AccessDenied
         | 
| 129 | 
            +
                  raise AccessDeniedS3Error.new(bucket: bucket_name, key: command.metadata[:key])
         | 
| 120 130 | 
             
                end
         | 
| 121 131 |  | 
| 122 | 
            -
                # Fetches  | 
| 123 | 
            -
                #  | 
| 124 | 
            -
                #
         | 
| 125 | 
            -
                #  | 
| 132 | 
            +
                # Fetches up to 100 previously-enqueued commands that are running or scheduled to run.
         | 
| 133 | 
            +
                # Commands are deserialized from metadata files in a well-known location in S3.
         | 
| 134 | 
            +
                # The metadata is created when the command is enqueued.
         | 
| 135 | 
            +
                # The instances in the exec environment update the metadata when executing a command, and remove the metadata when they are done.
         | 
| 126 136 | 
             
                #
         | 
| 127 | 
            -
                # Raises an error if the  | 
| 137 | 
            +
                # Raises an error if the application does not exist.
         | 
| 128 138 | 
             
                def enqueued_commands
         | 
| 129 | 
            -
                   | 
| 130 | 
            -
             | 
| 131 | 
            -
                    loop do
         | 
| 132 | 
            -
                      messages += sqs.receive_message(
         | 
| 133 | 
            -
                        queue_url: exec_queue_url,
         | 
| 134 | 
            -
                        max_number_of_messages: 10,
         | 
| 135 | 
            -
                        visibility_timeout: 5,
         | 
| 136 | 
            -
                        wait_time_seconds: 5,
         | 
| 137 | 
            -
                      ).messages
         | 
| 138 | 
            -
                    end
         | 
| 139 | 
            +
                  unless exists?
         | 
| 140 | 
            +
                    raise MissingApplicationError.new(application: self)
         | 
| 139 141 | 
             
                  end
         | 
| 140 | 
            -
                rescue Timeout::Error
         | 
| 141 | 
            -
                  messages.map { |msg| Exec::Command.from_json(msg.body) }
         | 
| 142 | 
            -
                end
         | 
| 143 142 |  | 
| 144 | 
            -
                def exec_queue_url
         | 
| 145 | 
            -
                  stack.stack_output("ExecQueueUrl")
         | 
| 146 | 
            -
                end
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                # Fetches commands running in the Environment::Exec environment.
         | 
| 149 | 
            -
                # Commands are deserialized from metadata files in a well-known location in S3.
         | 
| 150 | 
            -
                # The instances update the metadata when executing a command, and remove the metadata when they are done.
         | 
| 151 | 
            -
                #
         | 
| 152 | 
            -
                # Raises an error if the exec environment cannot be found.
         | 
| 153 | 
            -
                def running_commands
         | 
| 154 143 | 
             
                  # Ignoring truncation for simplicity; >100 commands will just have to suffer.
         | 
| 155 144 | 
             
                  objects = s3.list_objects_v2(
         | 
| 156 145 | 
             
                    bucket: bucket_name,
         | 
| @@ -168,6 +157,36 @@ module ElasticBeans | |
| 168 157 | 
             
                  }
         | 
| 169 158 | 
             
                end
         | 
| 170 159 |  | 
| 160 | 
            +
                # Fetches the +ExecQueueUrl+ Output from the application CloudFormation stack.
         | 
| 161 | 
            +
                # The stack must have the same name as the application.
         | 
| 162 | 
            +
                def exec_queue_url
         | 
| 163 | 
            +
                  stack.stack_output("ExecQueueUrl")
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                # Schedules the given ElasticBeans::Exec::Command or +id+ for termination.
         | 
| 167 | 
            +
                # Removes the metadata for the command and relies on the SQSConsumer to terminate it.
         | 
| 168 | 
            +
                #
         | 
| 169 | 
            +
                # Raises an error if the application does not exist.
         | 
| 170 | 
            +
                def kill_command(command_or_id)
         | 
| 171 | 
            +
                  unless exists?
         | 
| 172 | 
            +
                    raise MissingApplicationError.new(application: self)
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  if command_or_id.is_a?(ElasticBeans::Exec::Command)
         | 
| 176 | 
            +
                    command_id = command_or_id.id
         | 
| 177 | 
            +
                  else
         | 
| 178 | 
            +
                    command_id = command_or_id
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  key = "#{command_key_prefix}#{command_id}.json"
         | 
| 182 | 
            +
                  s3.delete_object(
         | 
| 183 | 
            +
                    bucket: bucket_name,
         | 
| 184 | 
            +
                    key: key,
         | 
| 185 | 
            +
                  )
         | 
| 186 | 
            +
                rescue ::Aws::S3::Errors::AccessDenied
         | 
| 187 | 
            +
                  raise AccessDeniedS3Error.new(bucket: bucket_name, key: key)
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 171 190 | 
             
                # Returns an ElasticBeans::ApplicationVersion for each version of the Elastic Beanstalk application.
         | 
| 172 191 | 
             
                def versions
         | 
| 173 192 | 
             
                  response = elastic_beanstalk.describe_application_versions(application_name: name)
         | 
| @@ -199,6 +218,7 @@ module ElasticBeans | |
| 199 218 |  | 
| 200 219 | 
             
                attr_reader :elastic_beanstalk, :s3, :stack
         | 
| 201 220 |  | 
| 221 | 
            +
                # Returns the S3 prefix under which command metadata is stored.
         | 
| 202 222 | 
             
                def command_key_prefix
         | 
| 203 223 | 
             
                  @command_key_prefix ||= "#{name}/exec/command/"
         | 
| 204 224 | 
             
                end
         | 
| @@ -217,8 +237,12 @@ module ElasticBeans | |
| 217 237 | 
             
                # :nodoc: all
         | 
| 218 238 | 
             
                # @!visibility private
         | 
| 219 239 | 
             
                class MissingApplicationError < ElasticBeans::Error
         | 
| 240 | 
            +
                  def initialize(application:)
         | 
| 241 | 
            +
                    @application = application
         | 
| 242 | 
            +
                  end
         | 
| 243 | 
            +
             | 
| 220 244 | 
             
                  def message
         | 
| 221 | 
            -
                    "Application `#{@ | 
| 245 | 
            +
                    "Application `#{@application.name}' does not exist. Please create the Elastic Beanstalk application using a CloudFormation stack."
         | 
| 222 246 | 
             
                  end
         | 
| 223 247 | 
             
                end
         | 
| 224 248 |  | 
| @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            require "aws-sdk"
         | 
| 2 2 | 
             
            require "elastic_beans/error"
         | 
| 3 | 
            +
            require "elastic_beans/error/access_denied"
         | 
| 3 4 |  | 
| 4 5 | 
             
            module ElasticBeans
         | 
| 5 6 | 
             
              # :nodoc: all
         | 
| @@ -39,6 +40,8 @@ module ElasticBeans | |
| 39 40 | 
             
                    s
         | 
| 40 41 | 
             
                  rescue ::Aws::CloudFormation::Errors::ValidationError
         | 
| 41 42 | 
             
                    raise MissingStackError.new(stack_name: stack_name)
         | 
| 43 | 
            +
                  rescue ::Aws::CloudFormation::Errors::AccessDenied
         | 
| 44 | 
            +
                    raise AccessDeniedCloudFormationError.new(stack_name: stack_name)
         | 
| 42 45 | 
             
                  end
         | 
| 43 46 |  | 
| 44 47 | 
             
                  class MissingStackError < ElasticBeans::Error
         | 
    
        data/lib/elastic_beans/cli.rb
    CHANGED
    
    | @@ -98,6 +98,21 @@ class ElasticBeans::CLI < Thor | |
| 98 98 | 
             
                error(e)
         | 
| 99 99 | 
             
              end
         | 
| 100 100 |  | 
| 101 | 
            +
              desc ElasticBeans::Command::Kill::USAGE, ElasticBeans::Command::Kill::DESC
         | 
| 102 | 
            +
              long_desc ElasticBeans::Command::Kill::LONG_DESC
         | 
| 103 | 
            +
              option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
         | 
| 104 | 
            +
              def kill(command_id)
         | 
| 105 | 
            +
                @verbose = options[:verbose]
         | 
| 106 | 
            +
                ElasticBeans::Command::Kill.new(
         | 
| 107 | 
            +
                  application: application(
         | 
| 108 | 
            +
                    name: options[:application],
         | 
| 109 | 
            +
                  ),
         | 
| 110 | 
            +
                  ui: ui,
         | 
| 111 | 
            +
                ).run(command_id)
         | 
| 112 | 
            +
              rescue StandardError => e
         | 
| 113 | 
            +
                error(e)
         | 
| 114 | 
            +
              end
         | 
| 115 | 
            +
             | 
| 101 116 | 
             
              desc ElasticBeans::Command::Ps::USAGE, ElasticBeans::Command::Ps::DESC
         | 
| 102 117 | 
             
              long_desc ElasticBeans::Command::Ps::LONG_DESC
         | 
| 103 118 | 
             
              option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
         | 
| @@ -21,8 +21,9 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an | |
| 21 21 | 
             
                  end
         | 
| 22 22 |  | 
| 23 23 | 
             
                  def run(*command_parts)
         | 
| 24 | 
            -
                     | 
| 25 | 
            -
                    application. | 
| 24 | 
            +
                    command = command(command_parts)
         | 
| 25 | 
            +
                    ui.info("Running `#{command.command_string}' on #{application.name}... (ID=#{command.id})")
         | 
| 26 | 
            +
                    application.enqueue_command(command)
         | 
| 26 27 | 
             
                  end
         | 
| 27 28 |  | 
| 28 29 | 
             
                  private
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            require "aws-sdk"
         | 
| 2 | 
            +
            require "elastic_beans/error/access_denied"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module ElasticBeans
         | 
| 5 | 
            +
              module Command
         | 
| 6 | 
            +
                # :nodoc: all
         | 
| 7 | 
            +
                class Kill
         | 
| 8 | 
            +
                  USAGE = "kill COMMAND_ID"
         | 
| 9 | 
            +
                  DESC = "Kill a running command or cancel a scheduled command that was enqueued with `exec`"
         | 
| 10 | 
            +
                  LONG_DESC = <<-LONG_DESC
         | 
| 11 | 
            +
            Kill a running command or cancel a scheduled command that was enqueued with `exec`.
         | 
| 12 | 
            +
            You can find an enqueued command's ID by running `ps`.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            Commands are run in an "exec" environment, separate from your webserver or worker environments.
         | 
| 15 | 
            +
            When they are enqueued, metadata is created in S3 and removed when the command is complete.
         | 
| 16 | 
            +
            Removing that metadata cancels the command.
         | 
| 17 | 
            +
            The command is sent a SIGTERM, and then a SIGKILL if it has not yet died.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
         | 
| 20 | 
            +
                  LONG_DESC
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def initialize(application:, ui:)
         | 
| 23 | 
            +
                    @application = application
         | 
| 24 | 
            +
                    @ui = ui
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def run(command_id)
         | 
| 28 | 
            +
                    ui.info("Scheduling command '#{command_id}' on #{application.name} for termination...")
         | 
| 29 | 
            +
                    application.kill_command(command_id)
         | 
| 30 | 
            +
                    ui.info("It may be a few moments before the command terminates.")
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  private
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  attr_reader :application, :ui
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -12,6 +12,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an | |
| 12 12 | 
             
                  LONG_DESC
         | 
| 13 13 |  | 
| 14 14 | 
             
                  COLUMNS = {
         | 
| 15 | 
            +
                    "COMMAND ID" => ->(command) { command.id },
         | 
| 15 16 | 
             
                    "INSTANCE" => ->(command) { command.instance_id || SCHEDULED_INSTANCE },
         | 
| 16 17 | 
             
                    "RUN TIME" => ->(command) {
         | 
| 17 18 | 
             
                      if command.start_time
         | 
| @@ -34,23 +35,14 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an | |
| 34 35 | 
             
                  end
         | 
| 35 36 |  | 
| 36 37 | 
             
                  def run
         | 
| 37 | 
            -
                     | 
| 38 | 
            -
                     | 
| 39 | 
            -
                    ui.debug { "Fetching enqueued and running commands from #{application.name}..." }
         | 
| 40 | 
            -
                    threads = [
         | 
| 41 | 
            -
                      Thread.new do
         | 
| 42 | 
            -
                        enqueued_commands = application.enqueued_commands
         | 
| 43 | 
            -
                      end,
         | 
| 44 | 
            -
                      Thread.new do
         | 
| 45 | 
            -
                        running_commands = application.running_commands
         | 
| 46 | 
            -
                      end,
         | 
| 47 | 
            -
                    ]
         | 
| 48 | 
            -
                    threads.each(&:join)
         | 
| 38 | 
            +
                    ui.debug { "Fetching enqueued commands from #{application.name}..." }
         | 
| 39 | 
            +
                    enqueued_commands = application.enqueued_commands
         | 
| 49 40 |  | 
| 41 | 
            +
                    enqueued_commands, running_commands = enqueued_commands.partition { |cmd| cmd.start_time.nil? }
         | 
| 50 42 | 
             
                    running_commands.sort_by!(&:start_time)
         | 
| 51 43 | 
             
                    commands = running_commands + enqueued_commands
         | 
| 52 44 | 
             
                    if commands.any?
         | 
| 53 | 
            -
                      ui.table(level: :info, columns: COLUMNS, rows:  | 
| 45 | 
            +
                      ui.table(level: :info, columns: COLUMNS, rows: commands)
         | 
| 54 46 | 
             
                    else
         | 
| 55 47 | 
             
                      ui.info("No commands enqueued or running")
         | 
| 56 48 | 
             
                    end
         | 
| @@ -2,6 +2,7 @@ require "elastic_beans/command/configure" | |
| 2 2 | 
             
            require "elastic_beans/command/create"
         | 
| 3 3 | 
             
            require "elastic_beans/command/deploy"
         | 
| 4 4 | 
             
            require "elastic_beans/command/exec"
         | 
| 5 | 
            +
            require "elastic_beans/command/kill"
         | 
| 5 6 | 
             
            require "elastic_beans/command/ps"
         | 
| 6 7 | 
             
            require "elastic_beans/command/restart"
         | 
| 7 8 | 
             
            require "elastic_beans/command/scale"
         | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            require "json"
         | 
| 2 2 | 
             
            require "aws-sdk"
         | 
| 3 3 | 
             
            require "elastic_beans/error"
         | 
| 4 | 
            +
            require "elastic_beans/error/access_denied"
         | 
| 4 5 |  | 
| 5 6 | 
             
            module ElasticBeans
         | 
| 6 7 | 
             
              # Interfaces with environment variable storage for the Elastic Beanstalk application.
         | 
| @@ -24,7 +25,7 @@ module ElasticBeans | |
| 24 25 | 
             
                rescue ::Aws::S3::Errors::NotFound
         | 
| 25 26 | 
             
                  {}
         | 
| 26 27 | 
             
                rescue ::Aws::S3::Errors::Forbidden
         | 
| 27 | 
            -
                  raise  | 
| 28 | 
            +
                  raise AccessDeniedS3Error.new(bucket: application.bucket_name, key: s3_key)
         | 
| 28 29 | 
             
                end
         | 
| 29 30 |  | 
| 30 31 | 
             
                # Updates the environment variables stored in S3 by merging it with the given +env_hash+.
         | 
| @@ -51,19 +52,5 @@ module ElasticBeans | |
| 51 52 | 
             
                def env_script(env_hash)
         | 
| 52 53 | 
             
                  JSON.dump(env_hash)
         | 
| 53 54 | 
             
                end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                # :nodoc: all
         | 
| 56 | 
            -
                # @!visibility private
         | 
| 57 | 
            -
                class CannotAccessConfigError < ElasticBeans::Error
         | 
| 58 | 
            -
                  def initialize(bucket_name:, key:)
         | 
| 59 | 
            -
                    @bucket_name = bucket_name
         | 
| 60 | 
            -
                    @key = key
         | 
| 61 | 
            -
                  end
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                  def message
         | 
| 64 | 
            -
                    "Cannot access configuration stored in S3 with bucket `#{@bucket_name}' and key `#{@key}'." \
         | 
| 65 | 
            -
                      " Please ask an administrator of your AWS account administrator to give you access."
         | 
| 66 | 
            -
                  end
         | 
| 67 | 
            -
                end
         | 
| 68 55 | 
             
              end
         | 
| 69 56 | 
             
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            require "elastic_beans/error"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # :nodoc: all
         | 
| 4 | 
            +
            # @!visibility private
         | 
| 5 | 
            +
            class AccessDeniedCloudFormationError < ElasticBeans::Error
         | 
| 6 | 
            +
              def initialize(stack_name:)
         | 
| 7 | 
            +
                @stack_name = stack_name
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def message
         | 
| 11 | 
            +
                <<-MESSAGE
         | 
| 12 | 
            +
            Access to CloudFormation stack '#{@stack_name}' was denied.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            Please check with your AWS administrator to give you full permission to access that stack in CloudFormation.
         | 
| 15 | 
            +
                MESSAGE
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            # :nodoc: all
         | 
| 20 | 
            +
            # @!visibility private
         | 
| 21 | 
            +
            class AccessDeniedS3Error < ElasticBeans::Error
         | 
| 22 | 
            +
              def initialize(bucket: nil, key: nil)
         | 
| 23 | 
            +
                @bucket = bucket
         | 
| 24 | 
            +
                @key = key
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def message
         | 
| 28 | 
            +
                msg = ""
         | 
| 29 | 
            +
                if @bucket || @key
         | 
| 30 | 
            +
                  msg << "Access to bucket '#{@bucket}' key '#{@key}' was denied.\n"
         | 
| 31 | 
            +
                else
         | 
| 32 | 
            +
                  msg << "Access to S3 was denied.\n"
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
                msg + <<-MESSAGE
         | 
| 35 | 
            +
            Please check with your AWS administrator to give you full permission to access elastic_beans resources in S3.
         | 
| 36 | 
            +
                MESSAGE
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -6,6 +6,8 @@ require "time" | |
| 6 6 | 
             
            # :nodoc: all
         | 
| 7 7 | 
             
            # @!visibility private
         | 
| 8 8 | 
             
            class SQSConsumer
         | 
| 9 | 
            +
              LOOP_PERIOD = 1
         | 
| 10 | 
            +
             | 
| 9 11 | 
             
              def initialize(instance_id:, logdir:, queue_url:, s3:, sqs:)
         | 
| 10 12 | 
             
                @instance_id = instance_id
         | 
| 11 13 | 
             
                @logdir = logdir
         | 
| @@ -23,9 +25,15 @@ class SQSConsumer | |
| 23 25 | 
             
                log("Starting message receive loop")
         | 
| 24 26 | 
             
                loop do
         | 
| 25 27 | 
             
                  begin
         | 
| 26 | 
            -
                    sleep | 
| 28 | 
            +
                    sleep(LOOP_PERIOD)
         | 
| 27 29 | 
             
                    if (message = receive_message)
         | 
| 28 30 | 
             
                      command = command_from_message(message)
         | 
| 31 | 
            +
                      unless metadata_exists?(command: command)
         | 
| 32 | 
            +
                        delete_message(message)
         | 
| 33 | 
            +
                        log_command("Skipping command ID=#{command['id']} `#{command['command']}' because it was killed")
         | 
| 34 | 
            +
                        next
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
             | 
| 29 37 | 
             
                      log_command("Executing command ID=#{command['id']} `#{command['command']}' on host #{`hostname`.chomp} pid #{command_pid}...")
         | 
| 30 38 | 
             
                      start_time = Time.now
         | 
| 31 39 | 
             
                      @command_pid = Process.spawn(
         | 
| @@ -35,10 +43,38 @@ class SQSConsumer | |
| 35 43 | 
             
                      )
         | 
| 36 44 | 
             
                      delete_message(message)
         | 
| 37 45 | 
             
                      update_metadata(command: command, start_time: start_time)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                      # poll command metadata to see if it is still available, if it disappears then kill the process
         | 
| 48 | 
            +
                      killed_thread = Thread.new do
         | 
| 49 | 
            +
                        loop do
         | 
| 50 | 
            +
                          sleep 5
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                          if !metadata_exists?(command: command)
         | 
| 53 | 
            +
                            if already_killed?
         | 
| 54 | 
            +
                              log_command(
         | 
| 55 | 
            +
                                "Sending KILL signal to command ID=#{command['id']} `#{command['command']}' on host #{`hostname`.chomp} pid #{command_pid}" \
         | 
| 56 | 
            +
                                  " because it was killed and has not terminated yet"
         | 
| 57 | 
            +
                              )
         | 
| 58 | 
            +
                              Process.kill("KILL", command_pid)
         | 
| 59 | 
            +
                              break
         | 
| 60 | 
            +
                            else
         | 
| 61 | 
            +
                              log_command(
         | 
| 62 | 
            +
                                "Sending TERM signal to command ID=#{command['id']} `#{command['command']}' on host #{`hostname`.chomp} pid #{command_pid}" \
         | 
| 63 | 
            +
                                  " because it was killed"
         | 
| 64 | 
            +
                              )
         | 
| 65 | 
            +
                              Process.kill("TERM", command_pid)
         | 
| 66 | 
            +
                              @killed = true
         | 
| 67 | 
            +
                            end
         | 
| 68 | 
            +
                          end
         | 
| 69 | 
            +
                        end
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
             | 
| 38 72 | 
             
                      _, status = Process.wait2(command_pid)
         | 
| 73 | 
            +
                      killed_thread.kill if killed_thread.alive?
         | 
| 39 74 | 
             
                      log_command("Command ID=#{command['id']} `#{command['command']}' exited on host #{`hostname`.chomp}: #{status}")
         | 
| 40 75 |  | 
| 41 76 | 
             
                      @command_pid = nil
         | 
| 77 | 
            +
                      @killed = nil
         | 
| 42 78 | 
             
                      remove_metadata(command: command)
         | 
| 43 79 | 
             
                    end
         | 
| 44 80 | 
             
                  rescue StandardError => e
         | 
| @@ -89,6 +125,10 @@ class SQSConsumer | |
| 89 125 | 
             
                $stderr.puts e.backtrace.join("\n")
         | 
| 90 126 | 
             
              end
         | 
| 91 127 |  | 
| 128 | 
            +
              def already_killed?
         | 
| 129 | 
            +
                @killed
         | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
             | 
| 92 132 | 
             
              def receive_message
         | 
| 93 133 | 
             
                sqs.receive_message(
         | 
| 94 134 | 
             
                  queue_url: queue_url,
         | 
| @@ -123,6 +163,21 @@ class SQSConsumer | |
| 123 163 | 
             
                log("[update_metadata] #{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}")
         | 
| 124 164 | 
             
              end
         | 
| 125 165 |  | 
| 166 | 
            +
              def metadata_exists?(command:)
         | 
| 167 | 
            +
                metadata = command['metadata']
         | 
| 168 | 
            +
                if metadata && metadata['bucket'] && metadata['key']
         | 
| 169 | 
            +
                  s3.head_object(
         | 
| 170 | 
            +
                    bucket: metadata['bucket'],
         | 
| 171 | 
            +
                    key: metadata['key'],
         | 
| 172 | 
            +
                  )
         | 
| 173 | 
            +
                  true
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
              rescue ::Aws::S3::Errors::NotFound
         | 
| 176 | 
            +
                false
         | 
| 177 | 
            +
              rescue StandardError => e
         | 
| 178 | 
            +
                log("[metadata_exists] #{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}")
         | 
| 179 | 
            +
              end
         | 
| 180 | 
            +
             | 
| 126 181 | 
             
              def remove_metadata(command:)
         | 
| 127 182 | 
             
                metadata = command['metadata']
         | 
| 128 183 | 
             
                if metadata && metadata['bucket'] && metadata['key']
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: elastic_beans
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.10.0. | 
| 4 | 
            +
              version: 0.10.0.alpha2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Adam Stegman
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2017-04- | 
| 11 | 
            +
            date: 2017-04-24 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: aws-sdk
         | 
| @@ -114,14 +114,14 @@ dependencies: | |
| 114 114 | 
             
                requirements:
         | 
| 115 115 | 
             
                - - "~>"
         | 
| 116 116 | 
             
                  - !ruby/object:Gem::Version
         | 
| 117 | 
            -
                    version: 0. | 
| 117 | 
            +
                    version: 0.8.0
         | 
| 118 118 | 
             
              type: :runtime
         | 
| 119 119 | 
             
              prerelease: false
         | 
| 120 120 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 121 121 | 
             
                requirements:
         | 
| 122 122 | 
             
                - - "~>"
         | 
| 123 123 | 
             
                  - !ruby/object:Gem::Version
         | 
| 124 | 
            -
                    version: 0. | 
| 124 | 
            +
                    version: 0.8.0
         | 
| 125 125 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 126 126 | 
             
              name: bundler
         | 
| 127 127 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -173,6 +173,7 @@ executables: | |
| 173 173 | 
             
            extensions: []
         | 
| 174 174 | 
             
            extra_rdoc_files: []
         | 
| 175 175 | 
             
            files:
         | 
| 176 | 
            +
            - ".circleci/config.yml"
         | 
| 176 177 | 
             
            - ".gitignore"
         | 
| 177 178 | 
             
            - ".rspec"
         | 
| 178 179 | 
             
            - Gemfile
         | 
| @@ -181,7 +182,6 @@ files: | |
| 181 182 | 
             
            - Rakefile
         | 
| 182 183 | 
             
            - bin/console
         | 
| 183 184 | 
             
            - bin/setup
         | 
| 184 | 
            -
            - circle.yml
         | 
| 185 185 | 
             
            - elastic_beans.gemspec
         | 
| 186 186 | 
             
            - exe/beans
         | 
| 187 187 | 
             
            - lib/elastic_beans.rb
         | 
| @@ -196,6 +196,7 @@ files: | |
| 196 196 | 
             
            - lib/elastic_beans/command/deploy.rb
         | 
| 197 197 | 
             
            - lib/elastic_beans/command/exec.rb
         | 
| 198 198 | 
             
            - lib/elastic_beans/command/get_env.rb
         | 
| 199 | 
            +
            - lib/elastic_beans/command/kill.rb
         | 
| 199 200 | 
             
            - lib/elastic_beans/command/ps.rb
         | 
| 200 201 | 
             
            - lib/elastic_beans/command/restart.rb
         | 
| 201 202 | 
             
            - lib/elastic_beans/command/scale.rb
         | 
| @@ -219,6 +220,7 @@ files: | |
| 219 220 | 
             
            - lib/elastic_beans/environment/webserver.rb
         | 
| 220 221 | 
             
            - lib/elastic_beans/environment/worker.rb
         | 
| 221 222 | 
             
            - lib/elastic_beans/error.rb
         | 
| 223 | 
            +
            - lib/elastic_beans/error/access_denied.rb
         | 
| 222 224 | 
             
            - lib/elastic_beans/error/environments_not_ready.rb
         | 
| 223 225 | 
             
            - lib/elastic_beans/exec.rb
         | 
| 224 226 | 
             
            - lib/elastic_beans/exec/command.rb
         | 
    
        data/circle.yml
    DELETED
    
    | @@ -1,24 +0,0 @@ | |
| 1 | 
            -
            ---
         | 
| 2 | 
            -
            machine:
         | 
| 3 | 
            -
              environment:
         | 
| 4 | 
            -
                RSPEC_OPTS: "-r rspec_junit_formatter --format progress --format RspecJunitFormatter -o $CIRCLE_TEST_REPORTS/rspec/junit.xml"
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            dependencies:
         | 
| 7 | 
            -
              override:
         | 
| 8 | 
            -
                - |
         | 
| 9 | 
            -
                  case $CIRCLE_NODE_INDEX in
         | 
| 10 | 
            -
                    0)
         | 
| 11 | 
            -
                      rvm-exec 2.3.1 bash -c "bundle check --path=vendor/bundle_2.3 || bundle install --path=vendor/bundle_2.3 --jobs=4 --retry=3"
         | 
| 12 | 
            -
                      ;;
         | 
| 13 | 
            -
                    1)
         | 
| 14 | 
            -
                      rvm-exec 2.2.5 bash -c "bundle check --path=vendor/bundle_2.2 || bundle install --path=vendor/bundle_2.2 --jobs=4 --retry=3"
         | 
| 15 | 
            -
                      ;;
         | 
| 16 | 
            -
                  esac
         | 
| 17 | 
            -
              post:
         | 
| 18 | 
            -
              - git config --global user.name "CircleCI"
         | 
| 19 | 
            -
              - git config --global user.email "ops-ro@onemedical.com"
         | 
| 20 | 
            -
             | 
| 21 | 
            -
            test:
         | 
| 22 | 
            -
              override:
         | 
| 23 | 
            -
                - case $CIRCLE_NODE_INDEX in 0) rvm-exec 2.3.1 bash -c "bundle check --path=vendor/bundle_2.3 && bundle exec rake" ;; 1) rvm-exec 2.2.5 bash -c "bundle check --path=vendor/bundle_2.2 && bundle exec rake" ;; esac:
         | 
| 24 | 
            -
                    parallel: true
         |