diff --git a/Gemfile.lock b/Gemfile.lock index dec25f4..b20ba86 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,16 @@ PATH remote: . specs: - rebi (0.2.2) + rebi (0.3.0) activesupport (~> 5.0) - aws-sdk (~> 2.10) + aws-sdk-ec2 (~> 1.0) + aws-sdk-elasticbeanstalk (~> 1.0) + aws-sdk-iam (~> 1.0) + aws-sdk-s3 (~> 1.0) colorize (~> 0.8) commander (~> 4.4) dotenv (~> 2.1) + pathspec (~> 0.2) rubyzip (~> 1.2) subprocess (~> 1.3) @@ -18,16 +22,31 @@ GEM i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - aws-sdk (2.10.99) - aws-sdk-resources (= 2.10.99) - aws-sdk-core (2.10.99) + aws-partitions (1.55.0) + aws-sdk-core (3.14.0) + aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.10.99) - aws-sdk-core (= 2.10.99) + aws-sdk-ec2 (1.25.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-elasticbeanstalk (1.3.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-iam (1.3.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-kms (1.4.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.8.0) + aws-sdk-core (~> 3) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.0) aws-sigv4 (1.0.2) + byebug (9.1.0) colorize (0.8.1) - commander (4.4.3) + commander (4.4.4) highline (~> 1.7.2) concurrent-ruby (1.0.5) dotenv (2.2.1) @@ -35,7 +54,8 @@ GEM i18n (0.9.1) concurrent-ruby (~> 1.0) jmespath (1.3.1) - minitest (5.10.3) + minitest (5.11.1) + pathspec (0.2.1) rubyzip (1.2.1) subprocess (1.3.2) thread_safe (0.3.6) @@ -46,6 +66,7 @@ PLATFORMS ruby DEPENDENCIES + byebug (~> 9) rebi! BUNDLED WITH diff --git a/README.md b/README.md index a07f455..0bd7ba4 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Deployment tool for Elasticbeanstalk # Features - - Switchable + multiple ebextensions folder + - Switchable + multiple ebextensions folder + - Switchable + ERB Supported Dockerrun.aws.json - Support erb in ebextension config files - Support env_file for environment variables - Multiple deployment - Deploy source code along with updating beanstalk options + - Hook commands before and after deploy - Simple config - Simple ssh @@ -17,28 +19,17 @@ $ gem install rebi ``` ## Usage -How to use my plugin. - -### AWS authentication -Rebi uses environment variables to get aws credentials -To set environment variables use `export` or `.env` file -```bash -# Use access key -export AWS_ACCESS_KEY_ID=xxxxx -export AWS_SECRET_ACCESS_KEY=xxxxxx -``` -Or -```bash -# Use profile -export AWS_PROFILE=xxxxx -``` - -Refer http://docs.aws.amazon.com/sdkforruby/api/Aws/ElasticBeanstalk/Client.html for other settings ### Yaml config -Default config file is `config/rebi.yml` use `-c` to switch +Default config file is `.rebi.yml` use `-c` to switch ```yaml app_name: app-name +profile: aws_profile # if use profile, can overwrite this by command option --profile + +# if use key/secret credentials(If you dont want to commit credentials to version control system use environment variables instead (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)) +aws_key: aws_key +aws_secret: aws_secret + stages: development: web: @@ -47,9 +38,9 @@ stages: ebextensions: "web-ebextensions" ``` -For other configs( key_name, instance_type, instance_num, service_role,...), please refer sample config +For other configs(key_name, instance_type, instance_num, service_role,...), please refer sample config ```bash -$ bundle exec rebi sample > rebi.yml +$ bundle exec rebi sample > .rebi.yml ``` ### Deploy diff --git a/bin/rebi b/bin/rebi index de8ed2e..e7e9ab8 100755 --- a/bin/rebi +++ b/bin/rebi @@ -4,6 +4,13 @@ require 'rubygems' require 'commander/import' require 'rebi' +# For debug +begin + require 'byebug' +rescue LoadError + +end + REBI_PATH = File.join(File.dirname(File.expand_path(__FILE__)), "..") program :name, 'Rebi' @@ -15,6 +22,11 @@ global_option('-c', '--config FILE', 'Load config data for your commands to use' Rebi.config.reload! end +global_option('--profile STRING', 'Aws profile') do |profile| + Rebi.config.aws_profile = profile + Rebi.config.reload! +end + command :deploy do |c| c.syntax = 'rebi deploy stage [env_name] [--options]' c.description = 'Deploy single or multiple ElasticBeanstalk environments' @@ -22,6 +34,8 @@ command :deploy do |c| c.example 'Deploy all environments in development', 'rebi deploy development' c.option '--include-settings', 'Deploy source and settings' c.option '--settings-only', 'Deploy option_settings and environment variables only' + c.option '--staged', 'Deploy files staged in git rather than the HEAD commit' + c.option '-y', '--yes', 'Skip confirmation' c.action do |args, options| stage, env_name = args raise Rebi::Error.new("Stage cannot be nil") if stage.blank? @@ -29,7 +43,7 @@ command :deploy do |c| if env_name.present? Rebi.app.deploy stage, env_name, opts else - if agree("Do you want to deploy all environments in #{stage} stage?(Y/n)") + if options.yes || agree("Do you want to deploy all environments in #{stage} stage?(Y/n)") Rebi.log("Preparing for deployment") Rebi.app.deploy stage, nil, opts end @@ -72,12 +86,13 @@ end command :terminate do |c| c.syntax = 'rebi terminate stage env_name' c.description = 'Terminate environment' + c.option '-y', '--yes', 'Skip confirmation' c.action do |args, options| stage, env_name = args raise Rebi::Error.new("Stage cannot be nil") if stage.blank? raise Rebi::Error.new("Env name cannot be nil") if env_name.blank? env_conf = Rebi.config.environment(stage, env_name) - if ask("Type '#{env_conf.name}' to confirm termination") == env_conf.name + if options.yes || agree("Do you want to terminate '#{env_conf.name}' environment?(Y/n)") Rebi.app.terminate! stage, env_name end end @@ -98,7 +113,6 @@ command :ssh do |c| c.action do |args, options| stage, env_name = args raise Rebi::Error.new("Stage cannot be nil") if stage.blank? - raise Rebi::Error.new("Env name cannot be nil") if env_name.blank? Rebi.app.ssh_interaction stage, env_name, options.__hash__ end end diff --git a/lib/rebi.rb b/lib/rebi.rb index dcdad7b..62d1bc6 100644 --- a/lib/rebi.rb +++ b/lib/rebi.rb @@ -1,5 +1,8 @@ require 'active_support/all' -require 'aws-sdk' +require 'aws-sdk-ec2' +require 'aws-sdk-s3' +require 'aws-sdk-elasticbeanstalk' +require 'aws-sdk-iam' require 'colorized_string' require 'singleton' require 'yaml' @@ -13,7 +16,9 @@ require 'thread' require 'thwait' require 'subprocess' +require 'pathspec' +require 'rebi/log' require 'rebi/erb_helper' require 'rebi/zip_helper' require 'rebi/application' @@ -24,11 +29,12 @@ require 'rebi/ec2' require 'rebi/version' -Dotenv.load +# Dotenv.load module Rebi - extend self + include Rebi::Log + extend self attr_accessor :config_file @config_file = "config/rebi.yml" @@ -36,18 +42,22 @@ def root Dir.pwd end - def client c=nil - @@client = c || Aws::ElasticBeanstalk::Client.new + def eb c=nil + @@eb = Aws::ElasticBeanstalk::Client.new end def ec2 - @@ec2_client = Rebi::EC2.new + @@ec2_client = Rebi::EC2.new Aws::EC2::Client.new end def iam @@iam_client = Aws::IAM::Client.new end + def s3 + @@s3_client = Aws::S3::Client.new + end + def app return Rebi::Application.get_or_create_application(config.app_name) end @@ -61,16 +71,4 @@ def reload! config.reload! end - def log mes, prefix=nil - puts "#{prefix ? "#{colorize_prefix(prefix)}: " : ""}#{mes}" - end - - COLORS = [:red, :green, :yellow, :blue, :magenta, :cyan, :white] - def colorize_prefix(prefix) - h = prefix.chars.inject(0) do |m, c| - m + c.ord - end - return ColorizedString[prefix].colorize(COLORS[h % COLORS.count]) - end - end diff --git a/lib/rebi/application.rb b/lib/rebi/application.rb index 9789e50..c797472 100644 --- a/lib/rebi/application.rb +++ b/lib/rebi/application.rb @@ -1,11 +1,11 @@ module Rebi class Application + include Rebi::Log attr_reader :app, :app_name, :client, :s3_client - def initialize app, client + def initialize app, client=Rebi.eb @app = app @app_name = app.application_name @client = client - @s3_client = Aws::S3::Client.new end def bucket_name @@ -16,18 +16,18 @@ def environments Rebi::Environment.all app_name end + def log_label + app_name + end + def deploy stage_name, env_name=nil, opts={} return deploy_stage(stage_name, opts) if env_name.blank? env = get_environment stage_name, env_name - app_version = create_app_version env begin - req_id = env.deploy app_version, opts - env.watch_request req_id if req_id - rescue Rebi::Error::EnvironmentInUpdating => e - Rebi.log("Environment in updating", env.name) - raise e + env.deploy opts + rescue Interrupt + log("Interrupt") end - req_id end def deploy_stage stage_name, opts={} @@ -38,9 +38,9 @@ def deploy_stage stage_name, opts={} begin deploy stage_name, env_name, opts rescue Exception => e - Rebi.log(e.message, "ERROR") - e.backtrace.each do |m| - Rebi.log(m, "ERROR") + error(e.message) + opts[:trace] && e.backtrace.each do |m| + error(m) end end end @@ -61,11 +61,11 @@ def print_environment_variables stage_name, env_name, from_config=false env = get_environment stage_name, env_name env_vars = from_config ? env.config.environment_variables : env.environment_variables - Rebi.log("#{from_config ? "Config" : "Current"} environment variables", env.name) + log("#{from_config ? "Config" : "Current"} environment variables") env_vars.each do |k,v| - Rebi.log("#{k}=#{v}") + log("#{k}=#{v}") end - Rebi.log("--------------------------------------", env.name) + log("--------------------------------------") end def print_environment_status stage_name, env_name @@ -79,11 +79,11 @@ def print_environment_status stage_name, env_name env = get_environment stage_name, env_name env.check_created! - Rebi.log("--------- CURRENT STATUS -------------", env.name) - Rebi.log("id: #{env.id}", env.name) - Rebi.log("Status: #{env.status}", env.name) - Rebi.log("Health: #{env.health}", env.name) - Rebi.log("--------------------------------------", env.name) + log("--------- CURRENT STATUS -------------") + log("id: #{env.id}") + log("Status: #{env.status}") + log("Health: #{env.health}") + log("--------------------------------------") end def terminate! stage_name, env_name @@ -92,37 +92,11 @@ def terminate! stage_name, env_name req_id = env.terminate! ThreadsWait.all_waits(env.watch_request req_id) if req_id rescue Rebi::Error::EnvironmentInUpdating => e - Rebi.log("Environment in updating", env.name) + log("Environment in updating") raise e end end - def create_app_version env - start = Time.now.utc - source_bundle = Rebi::ZipHelper.new.gen(env.config) - version_label = source_bundle[:label] - key = "#{app_name}/#{version_label}.zip" - Rebi.log("Uploading source bundle: #{version_label}.zip", env.config.name) - s3_client.put_object( - bucket: bucket_name, - key: key, - body: source_bundle[:file].read - ) - Rebi.log("Creating app version: #{version_label}", env.config.name) - client.create_application_version({ - application_name: app_name, - description: source_bundle[:message], - version_label: version_label, - source_bundle: { - s3_bucket: bucket_name, - s3_key: key - } - }) - Rebi.log("App version was created in: #{Time.now.utc - start}s", env.config.name) - return version_label - end - - def print_list others = [] configed = Hash.new {|h, k| h[k] = {} } @@ -135,36 +109,35 @@ def print_list end configed.each do |stg, envs| - Rebi.log "-------------" - Rebi.log "#{stg.camelize}:" - envs.each do |k, v| - Rebi.log "\t#{k.camelize}: #{v}" + log h1("#{stg.camelize}") + envs.each do |kname, env_name| + env = get_environment stg, kname + log "\t[#{kname.camelize}] #{env.name} #{h2(env.status)} #{hstatus(env.health)}" end end if others.present? - Rebi.log "-------------" - Rebi.log "Others:" + log h1("Others") others.each do |e| - Rebi.log "\t- #{e}" + log "\t- #{e}" end end end def ssh_interaction stage_name, env_name, opts={} + env_name = Rebi.config.stage(stage_name).keys.first unless env_name env = get_environment stage_name, env_name instance_ids = env.instance_ids return if instance_ids.empty? instance_ids.each.with_index do |i,idx| - Rebi.log "#{idx+1}) #{i}" + log "#{idx+1}) #{i}" end instance_id = instance_ids.first if instance_ids.count != 1 && opts[:select] - idx = 0 while idx < 1 || idx > instance_ids.count idx = ask_for_integer "Select an instance to ssh into:" @@ -172,7 +145,7 @@ def ssh_interaction stage_name, env_name, opts={} instance_id = instance_ids[idx - 1] end - Rebi.log "Preparing to ssh into [#{instance_id}]" + log "Preparing to ssh into [#{instance_id}]" env.ssh instance_id end @@ -181,7 +154,7 @@ def get_environment stage_name, env_name end def self.client - Rebi.client || Aws::ElasticBeanstalk::Client.new + Rebi.eb end def self.get_application app_name diff --git a/lib/rebi/config.rb b/lib/rebi/config.rb index 3534cfe..4e447ff 100644 --- a/lib/rebi/config.rb +++ b/lib/rebi/config.rb @@ -2,16 +2,38 @@ module Rebi class Config include Singleton + def initialize + reload! + end + def config_file - @config_file ||= "#{Rebi.root}/config/rebi.yml" + @config_file ||= "#{Rebi.root}/.rebi.yml" end def config_file=path @config_file = Pathname.new(path).realpath.to_s end + attr_writer :aws_profile, :aws_key, :aws_secret, :region + def aws_profile + @aws_profile || data[:profile] || ENV["AWS_PROFILE"] + end + + def aws_key + data[:aws_key] || ENV["AWS_ACCESS_KEY_ID"] + end + + def aws_secret + data[:aws_secret] || ENV["AWS_SECRET_ACCESS_KEY"] + end + + def region + data[:region] + end + def reload! @data = nil + set_aws_config return data end @@ -59,5 +81,27 @@ def data raise Rebi::Error::ConfigInvalid.new("stages cannot be nil") if @data[:stages].blank? return @data end + + def set_aws_config + conf = {} + + if region + conf.merge!({ + region: region + }) + end + + if aws_profile + conf.merge!({ + profile: aws_profile + }) + elsif aws_secret && aws_key + conf.merge!( + credentials: Aws::Credentials::new(aws_key, aws_secret) + ) + end + + Aws.config.update conf + end end end diff --git a/lib/rebi/config_environment.rb b/lib/rebi/config_environment.rb index 08b01fa..40f2ab2 100644 --- a/lib/rebi/config_environment.rb +++ b/lib/rebi/config_environment.rb @@ -61,7 +61,7 @@ def description end def cname_prefix - @cname_prefix ||= raw_conf[:cname_prefix] || "#{name}-#{stage}" + self.worker? ? nil : @cname_prefix || raw_conf[:cname_prefix] end def tier @@ -95,6 +95,10 @@ def worker? tier[:name] == "Worker" ? true : false end + def web? + !worker? + end + def instance_type get_opt ns[:autoscaling_launch], :InstanceType end @@ -135,6 +139,10 @@ def env_file @env_file ||= raw_conf[:env_file] end + def dockerrun + raw_conf[:dockerrun] + end + def cfg begin return nil if cfg_file.blank? @@ -222,6 +230,27 @@ def ns key=nil key.present? ? NAMESPACE[key.to_sym] : NAMESPACE end + def hooks + return @hooks if @hooks + + @hooks = {}.with_indifferent_access + + [:pre, :post].each do |type| + next unless h = raw_conf[:hooks][type] + @hooks[type] = h.is_a?(Array) ? h : [h] + end + + return @hooks + end + + def pre_hooks + hooks[:pre] + end + + def post_hooks + hooks[:post] + end + private def has_value_by_keys(hash, *keys) diff --git a/lib/rebi/ec2.rb b/lib/rebi/ec2.rb index ea56afd..4f4d028 100644 --- a/lib/rebi/ec2.rb +++ b/lib/rebi/ec2.rb @@ -1,12 +1,18 @@ module Rebi class EC2 + include Rebi::Log + attr_reader :client - def initialize client=Aws::EC2::Client.new + def initialize client @client = client end + def log_label + "EC2" + end + def describe_instance instance_id res = client.describe_instances instance_ids: [instance_id] return res.reservations.first.instances.first @@ -52,8 +58,5 @@ def authorize_ssh instance_id, &blk end end - def log mes - Rebi.log(mes, "EC2") - end end end diff --git a/lib/rebi/environment.rb b/lib/rebi/environment.rb index cd7ac59..a9a0031 100644 --- a/lib/rebi/environment.rb +++ b/lib/rebi/environment.rb @@ -1,6 +1,8 @@ module Rebi class Environment + include Rebi::Log + attr_reader :stage_name, :env_name, :app_name, @@ -8,6 +10,7 @@ class Environment :config attr_accessor :client, + :s3_client, :api_data @@ -41,15 +44,20 @@ class Environment 'ec2.sshalreadyopen': 'the specified rule "peer: 0.0.0.0/0, TCP, from port: 22, to port: 22,', } - def initialize stage_name, env_name, client=Rebi.client + def initialize stage_name, env_name, client=Rebi.eb @stage_name = stage_name @env_name = env_name @client = client + @s3_client = Rebi.s3 @config = Rebi.config.environment(stage_name, env_name) @app_name = @config.app_name @api_data end + def bucket_name + @bucket_name ||= client.create_storage_location.s3_bucket + end + def name created? ? api_data.environment_name : config.name end @@ -128,6 +136,8 @@ def response_msgs key=nil end def watch_request request_id + return unless request_id + log h1("WATCHING REQUEST [#{request_id}]") check_created! start = Time.now finished = false @@ -169,23 +179,39 @@ def instance_ids resp.environment_resources.instances.map(&:id).sort end + def create_app_version opts={} + return if opts[:settings_only] + start = Time.now.utc + source_bundle = Rebi::ZipHelper.new.gen(self.config, opts) + version_label = source_bundle[:label] + key = "#{app_name}/#{version_label}.zip" + log("Uploading source bundle: #{version_label}.zip") + s3_client.put_object( + bucket: bucket_name, + key: key, + body: source_bundle[:file].read + ) + log("Creating app version: #{version_label}") + client.create_application_version({ + application_name: app_name, + description: source_bundle[:message], + version_label: version_label, + source_bundle: { + s3_bucket: bucket_name, + s3_key: key + } + }) + log("App version was created in: #{Time.now.utc - start}s") + return version_label + end + def init version_label, opts={} - log("Creating new environment") + log h2("Start creating new environment") start_time = Time.now self.check_instance_profile - self.api_data = client.create_environment({ - application_name: config.app_name, - environment_name: config.name, - version_label: version_label, - tier: config.tier, - description: config.description, - option_settings: config.opts_array, - }.merge( - config.worker? ? {} : { cname_prefix: config.cname_prefix } - ).merge(config.platform_arn ? { platform_arn: config.platform_arn } : { solution_stack_name: config.solution_stack_name }) - ) + self.api_data = client.create_environment _create_args(version_label, opts) request_id = events(start_time).select do |e| e.message.match(response_msgs('event.createstarting')) @@ -195,31 +221,11 @@ def init version_label, opts={} def update version_label, opts={} - raise Rebi::Error::EnvironmentInUpdating.new(name) if in_updating? - log("Start updating") + raise Rebi::Error::EnvironmentInUpdating.new("Environment is in updating: #{name}") if in_updating? + log h2("Start updating") start_time = Time.now - deploy_opts = gen_deploy_opts - deploy_args = { - application_name: config.app_name, - environment_name: config.name, - version_label: version_label, - description: config.description, - } - if opts[:include_settings] || opts[:settings_only] - deploy_args.merge!({ - option_settings: deploy_opts[:option_settings], - options_to_remove: deploy_opts[:options_to_remove], - }) - deploy_args.delete(:version_label) if opts[:settings_only] - else - deploy_args.merge!({ - option_settings: deploy_opts[:env_only], - options_to_remove: deploy_opts[:options_to_remove], - }) - end - - self.api_data = client.update_environment(deploy_args) + self.api_data = client.update_environment(_update_args(version_label, opts)) request_id = events(start_time).select do |e| e.message.match(response_msgs('event.updatestarting')) @@ -228,18 +234,23 @@ def update version_label, opts={} return request_id end - def deploy version_label, opts={} + def deploy opts={} + _run_hooks :pre + version_label = create_app_version opts request_id = if created? update version_label, opts else init version_label, opts end + log h3("DEPLOYING") + _run_hooks :post + watch_request request_id return request_id end def terminate! check_created - log("Start terminating") + log h2("Start terminating") client.terminate_environment({ environment_name: name, environment_id: id, @@ -252,8 +263,8 @@ def terminate! return request_id end - def log mes - Rebi.log(mes, name) + def log_label + name end def success_message? mes @@ -280,31 +291,6 @@ def success_message? mes return false end - def gen_deploy_opts - to_deploy = [] - to_remove = [] - env_only = [] - config.opts_array.each do |o| - o = o.deep_dup - - if o[:namespace] == config.ns(:app_env) - if o[:value].blank? - o.delete(:value) - to_remove << o - next - else - env_only << o - end - end - to_deploy << o - end - return { - option_settings: to_deploy, - options_to_remove: to_remove, - env_only: env_only, - } - end - def ssh instance_id raise "Invalid instance_id" unless self.instance_ids.include?(instance_id) @@ -390,15 +376,96 @@ def self.create stage_name, env_name, version_label, client end # TODO - def self.all app_name, client=Rebi.client + def self.all app_name, client=Rebi.eb client.describe_environments(application_name: app_name, include_deleted: false).environments end - def self.get stage_name, env_name, client=Rebi.client + def self.get stage_name, env_name, client=Rebi.eb env = new stage_name, env_name, client return env.created? ? env : nil end + private + + def _create_args version_label, opts={} + args = { + application_name: config.app_name, + environment_name: config.name, + version_label: version_label, + tier: config.tier, + description: config.description, + option_settings: config.opts_array, + } + + args.merge!(cname_prefix: config.cname_prefix) if config.cname_prefix + + args.merge!(config.platform_arn ? { platform_arn: config.platform_arn } : { solution_stack_name: config.solution_stack_name }) + return args + end + + def _update_args version_label, opts={} + deploy_opts = _gen_deploy_opts + + args = { + application_name: config.app_name, + environment_name: config.name, + description: config.description, + } + + args.merge!(version_label: version_label) if version_label + + if opts[:include_settings] || opts[:settings_only] + args.merge!({ + option_settings: deploy_opts[:option_settings], + options_to_remove: deploy_opts[:options_to_remove], + }) + args.delete(:version_label) if opts[:settings_only] + else + args.merge!({ + option_settings: deploy_opts[:env_only], + options_to_remove: deploy_opts[:options_to_remove], + }) + end + + return args + end + + def _gen_deploy_opts + to_deploy = [] + to_remove = [] + env_only = [] + config.opts_array.each do |o| + o = o.deep_dup + + if o[:namespace] == config.ns(:app_env) + if o[:value].blank? + o.delete(:value) + to_remove << o + next + else + env_only << o + end + end + to_deploy << o + end + return { + option_settings: to_deploy, + options_to_remove: to_remove, + env_only: env_only, + } + end + + def _run_hooks type + if hooks = config.hooks[type] + log h1("RUNNING #{type.upcase} HOOKS") + hooks.each do |cmd| + log h4(cmd) + system "#{cmd} 2>&1" + raise "Command failed" unless $?.success? + end + log h1("#{type.upcase} HOOKS FINISHED!!!") + end + end end end diff --git a/lib/rebi/iam.rb b/lib/rebi/iam.rb deleted file mode 100644 index 06f22db..0000000 --- a/lib/rebi/iam.rb +++ /dev/null @@ -1,43 +0,0 @@ -# module Rebi -# class IAM -# -# attr_reader :client -# -# def initialize client=Aws::IAM::Client.new -# @client = client -# end -# -# def check_or_create_eb_profile profile -# -# end -# -# def create_instance_profile profile -# clieng.create_instance_profile({ -# instance_profile_name: profile -# }) -# end -# -# def get_default_role -# role = Rebi::ConfigEnvironment::DEFAULT_IAM_INSTANCE_PROFILE -# document = '{"Version": "2008-10-17","Statement": [{"Action":' \ -# ' "sts:AssumeRole","Principal": {"Service": ' \ -# '"ec2.amazonaws.com"},"Effect": "Allow","Sid": ""}]}' -# client.create_role({ -# role_name: role, -# assume_role_policy_document: document -# }) -# return role -# end -# -# def add_role_to_profile profile, role -# client.add_role_to_instance_profile({ -# instance_profile_name: profile, -# role_name: role -# }) -# end -# -# def log mes -# Rebi.log(mes, "IAM") -# end -# end -# end diff --git a/lib/rebi/log.rb b/lib/rebi/log.rb new file mode 100644 index 0000000..13e5fb2 --- /dev/null +++ b/lib/rebi/log.rb @@ -0,0 +1,54 @@ +module Rebi + module Log + def log mes, label=self.log_label + puts "#{label ? "#{colorize_prefix(label)}: " : ""}#{mes}" + end + + def error mes, label=self.error_label + puts colorize(label, color: :white, background: :red) + ": " + mes + end + + def log_label + "Rebi" + end + + def error_label + "ERROR" + end + + def colorize_prefix(prefix) + colors = ColorizedString.colors + colors.delete :light_black + h = prefix.chars.inject(0) do |m, c| + m + c.ord + end + return colorize(prefix, color: colors[h % colors.count], background: :light_black) + end + + def h1 s + colorize(s, color: :light_yellow, background: :light_blue) + end + + def h2 s + colorize(s, color: :light_blue, background: :light_cyan) + end + + def h3 s + colorize(s, color: :light_yellow, background: :light_blue, mode: :bold) + end + + def h4 s + colorize(s, color: :black, background: :green, mode: :italic) + end + + def hstatus s + bg = s.downcase.to_sym + bg = :light_black unless ColorizedString.colors.include?(bg) + colorize(s, color: :black, background: bg, mode: :italic) + end + + def colorize mes, opts={} + ColorizedString[mes].colorize(opts) + end + end +end diff --git a/lib/rebi/version.rb b/lib/rebi/version.rb index a9d3018..b951b50 100644 --- a/lib/rebi/version.rb +++ b/lib/rebi/version.rb @@ -1,3 +1,3 @@ module Rebi - VERSION = '0.2.2' + VERSION = '0.3.0' end diff --git a/lib/rebi/zip_helper.rb b/lib/rebi/zip_helper.rb index dc4bd86..70bc981 100644 --- a/lib/rebi/zip_helper.rb +++ b/lib/rebi/zip_helper.rb @@ -1,41 +1,64 @@ module Rebi class ZipHelper + include Rebi::Log + + EB_IGNORE = ".ebignore" + def initialize - `git status` - raise Rebi::Error::NoGit.new("Not a git repository") unless $?.success? + # raise Rebi::Error::NoGit.new("Not a git repository") unless git? + end + + def git? + `git status 2>&1` + $?.success? end def ls_files - `git ls-files`.split("\n") + `git ls-files 2>&1`.split("\n") end def raw_zip_archive opts={} tmp_file = Tempfile.new("git_archive") - system "git archive --format=zip HEAD > #{tmp_file.path}" + if !git? || ebignore? + Zip::File.open(tmp_file.path, Zip::File::CREATE) do |z| + spec = ebignore_spec + Dir.glob("**/*").each do |f| + next if ebignore_spec.match f + if File.directory?(f) + z.mkdir f unless z.find_entry f + else + z.add f, f + end + end + end + else + commit_id = opts[:staged] ? `git write-tree`.chomp : "HEAD" + system "git archive --format=zip #{commit_id} > #{tmp_file.path}" + end return tmp_file end def version_label - `git describe --always --abbrev=8`.chomp + git? ? `git describe --always --abbrev=8`.chomp : SecureRandom.hex[0, 8] end def message - `git log --oneline -1`.chomp.split(" ")[1..-1].join(" ")[0..190] + git? ? `git log --oneline -1`.chomp.split(" ")[1..-1].join(" ")[0..190] : "Deploy #{Time.now.strftime("%Y/%m/%d %H:%M")}" end # Create zip archivement - def gen env_conf - Rebi.log("Creating zip archivement", env_conf.name) + def gen env_conf,opts={} + log("Creating zip archivement", env_conf.name) start = Time.now ebextensions = env_conf.ebextensions - files = ls_files - tmp_file = raw_zip_archive + tmp_file = raw_zip_archive opts tmp_folder = Dir.mktmpdir - Zip::File.open(tmp_file) do |z| + Zip::File.open(tmp_file.path) do |z| ebextensions.each do |ex_folder| Dir.glob("#{ex_folder}/*.config") do |fname| - next unless (File.file?(fname) && files.include?(fname)) + z.remove_folder ex_folder unless ex_folder == ".ebextension" + next unless File.file?(fname) next unless y = YAML::load(ErbHelper.new(File.read(fname), env_conf).result) basename = File.basename(fname) target = ".ebextensions/#{basename}" @@ -47,9 +70,25 @@ def gen env_conf z.add target, tmp_yaml end end + + dockerrun_file = env_conf.dockerrun || "Dockerrun.aws.json" + + if File.exists?(dockerrun_file) + dockerrun = JSON.parse ErbHelper.new(File.read(dockerrun_file), env_conf).result + tmp_dockerrun = "#{tmp_folder}/Dockerrun.aws.json" + File.open(tmp_dockerrun, 'w') do |f| + f.write dockerrun.to_json + end + z.remove env_conf.dockerrun if z.find_entry env_conf.dockerrun + z.remove "Dockerrun.aws.json" if z.find_entry "Dockerrun.aws.json" + z.add "Dockerrun.aws.json", tmp_dockerrun + end + end + FileUtils.rm_rf tmp_folder - Rebi.log("Zip was created in: #{Time.now - start}s", env_conf.name) + + log("Zip was created in: #{Time.now - start}s", env_conf.name) return { label: Time.now.strftime("app_#{env_conf.name}_#{version_label}_%Y%m%d_%H%M%S"), file: File.open(tmp_file.path), @@ -57,5 +96,32 @@ def gen env_conf } end + def ebignore_spec + if ebignore? + path_spec = PathSpec.from_filename(EB_IGNORE) + path_spec.add(".git") + return path_spec + else + return PathSpec.new(".git") + end + end + + def ebignore? + File.exist?(EB_IGNORE) + end + + end +end + +module Zip + class File + def remove_folder fname + if folder = find_entry(fname) + remove folder if folder.directory? + end + glob("#{fname}/**/*").each do |f| + remove f + end + end end end diff --git a/rebi.gemspec b/rebi.gemspec index 93617b4..0e26bdb 100644 --- a/rebi.gemspec +++ b/rebi.gemspec @@ -19,11 +19,19 @@ Gem::Specification.new do |s| s.executables = ["rebi"] s.add_dependency "rubyzip", "~> 1.2" - s.add_dependency "aws-sdk", "~> 2.10" + + # s.add_dependency 'aws-sdk', "~> 3.0" + s.add_dependency 'aws-sdk-ec2', "~> 1.0" + s.add_dependency 'aws-sdk-s3', "~> 1.0" + s.add_dependency 'aws-sdk-elasticbeanstalk', "~> 1.0" + s.add_dependency 'aws-sdk-iam', "~> 1.0" + s.add_dependency "dotenv", "~> 2.1" s.add_dependency "colorize", "~> 0.8" s.add_dependency "activesupport", "~> 5.0" s.add_dependency "commander", "~> 4.4" s.add_dependency "subprocess", "~> 1.3" + s.add_dependency "pathspec", "~> 0.2" + s.add_development_dependency "byebug", "~> 9" end diff --git a/sample/rebi.yml b/sample/rebi.yml index 9920179..ccb4654 100644 --- a/sample/rebi.yml +++ b/sample/rebi.yml @@ -1,6 +1,12 @@ app_name: App_name # Required app_description: Description # Optional +profile: aws_profile # if use profile config, overwrite with --profile option + +# If use credentials(key, secret) +aws_key: aws_key +aws_secret: aws_secret + stages: #Required: Hash development: #Stage name web: # Env name @@ -33,8 +39,13 @@ stages: #Required: Hash # Result will has 01.config, 02.config(from web1_extensions) and 03.config cfg_file: File path or cfg name # Optional + dockerrun: Dockerrun.aws.json.stg # Dockerrun file options: #Hash, Other custom options for using in erb use_basic: true # rebi.opts.use_basic + + hooks: + pre: ls # String or Array, run before upload source bundle + post: ls # String or Array, run right after sending deploy request worker: # ...