forger 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.gitmodules +0 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +147 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +136 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +249 -0
  11. data/Rakefile +6 -0
  12. data/docs/example/.env +2 -0
  13. data/docs/example/.env.development +2 -0
  14. data/docs/example/.env.production +3 -0
  15. data/docs/example/app/scripts/hello.sh +3 -0
  16. data/docs/example/app/user-data/bootstrap.sh +35 -0
  17. data/docs/example/config/development.yml +8 -0
  18. data/docs/example/profiles/default.yml +11 -0
  19. data/docs/example/profiles/spot.yml +20 -0
  20. data/exe/forger +14 -0
  21. data/forger.gemspec +38 -0
  22. data/lib/forger.rb +29 -0
  23. data/lib/forger/ami.rb +10 -0
  24. data/lib/forger/aws_service.rb +7 -0
  25. data/lib/forger/base.rb +42 -0
  26. data/lib/forger/clean.rb +13 -0
  27. data/lib/forger/cleaner.rb +5 -0
  28. data/lib/forger/cleaner/ami.rb +45 -0
  29. data/lib/forger/cli.rb +67 -0
  30. data/lib/forger/command.rb +67 -0
  31. data/lib/forger/completer.rb +161 -0
  32. data/lib/forger/completer/script.rb +6 -0
  33. data/lib/forger/completer/script.sh +10 -0
  34. data/lib/forger/config.rb +20 -0
  35. data/lib/forger/core.rb +51 -0
  36. data/lib/forger/create.rb +155 -0
  37. data/lib/forger/create/error_messages.rb +58 -0
  38. data/lib/forger/create/params.rb +106 -0
  39. data/lib/forger/dotenv.rb +30 -0
  40. data/lib/forger/help.rb +9 -0
  41. data/lib/forger/help/ami.md +13 -0
  42. data/lib/forger/help/clean/ami.md +22 -0
  43. data/lib/forger/help/compile.md +5 -0
  44. data/lib/forger/help/completion.md +22 -0
  45. data/lib/forger/help/completion_script.md +3 -0
  46. data/lib/forger/help/create.md +7 -0
  47. data/lib/forger/help/upload.md +10 -0
  48. data/lib/forger/help/wait/ami.md +12 -0
  49. data/lib/forger/hook.rb +33 -0
  50. data/lib/forger/profile.rb +64 -0
  51. data/lib/forger/script.rb +46 -0
  52. data/lib/forger/script/compile.rb +40 -0
  53. data/lib/forger/script/compress.rb +62 -0
  54. data/lib/forger/script/templates/ami_creation.sh +12 -0
  55. data/lib/forger/script/templates/auto_terminate.sh +11 -0
  56. data/lib/forger/script/templates/auto_terminate_after_timeout.sh +5 -0
  57. data/lib/forger/script/templates/cloudwatch.sh +3 -0
  58. data/lib/forger/script/templates/extract_aws_ec2_scripts.sh +48 -0
  59. data/lib/forger/script/upload.rb +99 -0
  60. data/lib/forger/scripts/auto_terminate.sh +14 -0
  61. data/lib/forger/scripts/auto_terminate/after_timeout.sh +18 -0
  62. data/lib/forger/scripts/auto_terminate/functions.sh +130 -0
  63. data/lib/forger/scripts/auto_terminate/functions/amazonlinux2.sh +10 -0
  64. data/lib/forger/scripts/auto_terminate/functions/ubuntu.sh +11 -0
  65. data/lib/forger/scripts/auto_terminate/setup.sh +31 -0
  66. data/lib/forger/scripts/cloudwatch.sh +24 -0
  67. data/lib/forger/scripts/cloudwatch/configure.sh +84 -0
  68. data/lib/forger/scripts/cloudwatch/install.sh +3 -0
  69. data/lib/forger/scripts/cloudwatch/install/amazonlinux2.sh +4 -0
  70. data/lib/forger/scripts/cloudwatch/install/ubuntu.sh +23 -0
  71. data/lib/forger/scripts/cloudwatch/service.sh +3 -0
  72. data/lib/forger/scripts/cloudwatch/service/amazonlinux2.sh +11 -0
  73. data/lib/forger/scripts/cloudwatch/service/ubuntu.sh +8 -0
  74. data/lib/forger/scripts/shared/functions.sh +78 -0
  75. data/lib/forger/setting.rb +52 -0
  76. data/lib/forger/template.rb +13 -0
  77. data/lib/forger/template/context.rb +32 -0
  78. data/lib/forger/template/helper.rb +17 -0
  79. data/lib/forger/template/helper/ami_helper.rb +33 -0
  80. data/lib/forger/template/helper/core_helper.rb +127 -0
  81. data/lib/forger/template/helper/partial_helper.rb +71 -0
  82. data/lib/forger/template/helper/script_helper.rb +53 -0
  83. data/lib/forger/template/helper/ssh_key_helper.rb +21 -0
  84. data/lib/forger/version.rb +3 -0
  85. data/lib/forger/wait.rb +12 -0
  86. data/lib/forger/waiter.rb +5 -0
  87. data/lib/forger/waiter/ami.rb +61 -0
  88. data/spec/fixtures/demo_project/config/settings.yml +22 -0
  89. data/spec/fixtures/demo_project/config/test.yml +9 -0
  90. data/spec/fixtures/demo_project/profiles/default.yml +33 -0
  91. data/spec/lib/cli_spec.rb +41 -0
  92. data/spec/lib/params_spec.rb +71 -0
  93. data/spec/spec_helper.rb +33 -0
  94. metadata +354 -0
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new
data/docs/example/.env ADDED
@@ -0,0 +1,2 @@
1
+ # Example .env, meant to be updated.
2
+ # Variables in here are available and shared across all environments: development, production, etc.
@@ -0,0 +1,2 @@
1
+ # Example .env.development, meant to be updated.
2
+ ENV_DEVELOPMENT_KEY=example1
@@ -0,0 +1,3 @@
1
+ # Example .env.production, meant to be updated.
2
+ # This file is normally .gitignore.
3
+ MYKEY=example1
@@ -0,0 +1,3 @@
1
+ #!/bin/bash -exu
2
+
3
+ echo "hello world"
@@ -0,0 +1,35 @@
1
+ #!/bin/bash -exu
2
+
3
+ export HOME=/root # user-data env runs in weird shell where user is root but HOME is not set
4
+
5
+ sudo yum install -y postgresql
6
+
7
+ # https://gist.github.com/juno/1330165
8
+ # Install developer tools
9
+ yum install -y git gcc make readline-devel openssl-devel
10
+
11
+ # Install ruby-build system-widely
12
+ git clone git://github.com/sstephenson/ruby-build.git /tmp/ruby-build
13
+ cd /tmp/ruby-build
14
+ ./install.sh
15
+ echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc
16
+
17
+ # Install rbenv for root
18
+ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
19
+ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
20
+ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
21
+ set +u
22
+ source ~/.bashrc
23
+ set -u
24
+
25
+ # Install and enable ruby
26
+ rbenv install 2.5.0
27
+
28
+ # Install ruby for ec2-user also
29
+ cp -R ~/.rbenv /home/ec2-user/
30
+ chown -R ec2-user:ec2-user /home/ec2-user/.rbenv
31
+ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> /home/ec2-user/.bashrc
32
+ echo 'eval "$(rbenv init -)"' >> /home/ec2-user/.bashrc
33
+ echo '2.5.0' > /home/ec2-user/.ruby-version
34
+
35
+ uptime | tee /var/log/boot-time.log
@@ -0,0 +1,8 @@
1
+ ---
2
+ vpc_id: vpc-111
3
+ subnets:
4
+ - subnet-111
5
+ - subnet-222
6
+ - subnet-333
7
+ security_group_ids:
8
+ - sg-111
@@ -0,0 +1,11 @@
1
+ ---
2
+ image_id: ami-97785bed
3
+ instance_type: t2.medium
4
+ key_name: default
5
+ max_count: 1
6
+ min_count: 1
7
+ security_group_ids: <%= config["security_group_ids"] %>
8
+ subnet_id: <%= config["subnets"].shuffle.first %>
9
+ user_data: "<%= user_data("bootstrap") %>"
10
+ iam_instance_profile:
11
+ name: IAMProfileName
@@ -0,0 +1,20 @@
1
+ ---
2
+ image_id: ami-97785bed
3
+ instance_type: t2.medium
4
+ key_name: default
5
+ max_count: 1
6
+ min_count: 1
7
+ security_group_ids:
8
+ - sg-111
9
+ subnet_id: <%= %w[subnet-111 subnet-222].shuffle.first %>
10
+ user_data: "<%= user_data("dev") %>"
11
+ iam_instance_profile:
12
+ name: IAMProfileName
13
+ instance_market_options:
14
+ market_type: spot
15
+ # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html
16
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html
17
+ spot_options:
18
+ max_price: "0.02"
19
+ spot_instance_type: one-time
20
+ # instance_interruption_behavior: hibernate
data/exe/forger ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Trap ^C
4
+ Signal.trap("INT") {
5
+ puts "\nCtrl-C detected. Exiting..."
6
+ sleep 1
7
+ exit
8
+ }
9
+
10
+ $:.unshift(File.expand_path("../../lib", __FILE__))
11
+ require "forger"
12
+ require "forger/cli"
13
+
14
+ Forger::CLI.start(ARGV)
data/forger.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "forger/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "forger"
8
+ spec.version = Forger::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tongueroo@gmail.com"]
11
+ spec.description = %q{Simple tool to create AWS ec2 instances consistently with pre-configured settings}
12
+ spec.summary = %q{Simple tool to create AWS ec2 instances}
13
+ spec.homepage = "https://github.com/tongueroo/forger"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "activesupport"
23
+ spec.add_dependency "aws-sdk-ec2"
24
+ spec.add_dependency "aws-sdk-s3"
25
+ spec.add_dependency "colorize"
26
+ spec.add_dependency "dotenv"
27
+ spec.add_dependency "filesize"
28
+ spec.add_dependency "hashie"
29
+ spec.add_dependency "render_me_pretty"
30
+ spec.add_dependency "thor"
31
+
32
+ spec.add_development_dependency "bundler"
33
+ spec.add_development_dependency "byebug"
34
+ spec.add_development_dependency "guard"
35
+ spec.add_development_dependency "guard-bundler"
36
+ spec.add_development_dependency "guard-rspec"
37
+ spec.add_development_dependency "rake"
38
+ end
data/lib/forger.rb ADDED
@@ -0,0 +1,29 @@
1
+ $:.unshift(File.expand_path("../", __FILE__))
2
+ require "forger/version"
3
+ require "colorize"
4
+ require "render_me_pretty"
5
+
6
+ module Forger
7
+ autoload :Help, "forger/help"
8
+ autoload :Command, "forger/command"
9
+ autoload :CLI, "forger/cli"
10
+ autoload :AwsService, "forger/aws_service"
11
+ autoload :Profile, "forger/profile"
12
+ autoload :Base, "forger/base"
13
+ autoload :Create, "forger/create"
14
+ autoload :Ami, "forger/ami"
15
+ autoload :Wait, "forger/wait"
16
+ autoload :Clean, "forger/clean"
17
+ autoload :Template, "forger/template"
18
+ autoload :Script, "forger/script"
19
+ autoload :Config, "forger/config"
20
+ autoload :Core, "forger/core"
21
+ autoload :Dotenv, "forger/dotenv"
22
+ autoload :Hook, "forger/hook"
23
+ autoload :Completion, "forger/completion"
24
+ autoload :Completer, "forger/completer"
25
+ autoload :Setting, "forger/setting"
26
+ extend Core
27
+ end
28
+
29
+ Forger::Dotenv.load!
data/lib/forger/ami.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Forger
2
+ class Ami < Base
3
+ def run
4
+ # Delegates to the Create command.
5
+ # So we just have to set up the option for it.
6
+ @options[:ami_name] = @name
7
+ Create.new(@options).run
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'aws-sdk-ec2'
2
+
3
+ module Forger::AwsService
4
+ def ec2
5
+ @ec2 ||= Aws::EC2::Client.new
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ module Forger
2
+ class Base
3
+ # constants really only used by script classes
4
+ SCRIPTS_INFO_PATH = "tmp/data/scripts_info.txt"
5
+ BUILD_ROOT = "tmp"
6
+
7
+ def initialize(options={})
8
+ @options = options.clone
9
+ @name = randomize(@options[:name])
10
+ Forger.validate_in_project!
11
+ end
12
+
13
+ # Appends a short random string at the end of the ec2 instance name.
14
+ # Later we will strip this same random string from the name.
15
+ # Very makes it convenient. We can just type:
16
+ #
17
+ # forger create server --randomize
18
+ #
19
+ # instead of:
20
+ #
21
+ # forger create server-123 --profile server
22
+ #
23
+ def randomize(name)
24
+ if @options[:randomize]
25
+ random = (0...3).map { (65 + rand(26)).chr }.join.downcase # Ex: jhx
26
+ [name, random].join('-')
27
+ else
28
+ name
29
+ end
30
+ end
31
+
32
+ # Strip the random string at end of the ec2 instance name
33
+ def derandomize(name)
34
+ if @options[:randomize]
35
+ name.sub(/-(\w{3})$/,'') # strip the random part at the end
36
+ else
37
+ name
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module Forger
2
+ autoload :Cleaner, 'forger/cleaner'
3
+
4
+ class Clean < Command
5
+ desc "ami", "Clean until AMI available."
6
+ long_desc Help.text("clean:ami")
7
+ option :keep, type: :numeric, default: 2, desc: "Number of images to keep"
8
+ option :noop, type: :boolean, desc: "Noop or dry-run mode"
9
+ def ami(query)
10
+ Cleaner::Ami.new(options.merge(query: query)).clean
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Forger
2
+ module Cleaner
3
+ autoload :Ami, 'forger/cleaner/ami'
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ module Forger::Cleaner
2
+ class Ami < Forger::Base
3
+ include Forger::AwsService
4
+
5
+ def clean
6
+ query = @options[:query]
7
+ keep = @options[:keep] || 2
8
+ puts "Cleaning out old AMIs with base name: #{@options[:query]}"
9
+ return if ENV['TEST']
10
+
11
+ images = search_ami(query)
12
+ images = images.sort_by { |i| i.name }.reverse
13
+ delete_list = images[keep..-1] || []
14
+ puts "Deleting #{delete_list.size} images."
15
+ delete_list.each do |i|
16
+ delete(i)
17
+ end
18
+ end
19
+
20
+ private
21
+ def delete(image)
22
+ message = "Deleting image: #{image.image_id} #{image.name}"
23
+ if @options[:noop]
24
+ puts "NOOP: #{message}"
25
+ else
26
+ puts message
27
+ ec2.deregister_image(image_id: image.image_id)
28
+ end
29
+ rescue Aws::EC2::Errors::InvalidAMIIDUnavailable
30
+ # happens when image was just deleted but its still
31
+ # showing up as available when calling describe_images
32
+ puts "WARN: #{e.message}"
33
+ end
34
+
35
+ def search_ami(query, owners=["self"])
36
+ ec2.describe_images(
37
+ owners: owners,
38
+ filters: [
39
+ {name: "name", values: [query]},
40
+ {name: "state", values: ["available"]}
41
+ ]
42
+ ).images
43
+ end
44
+ end
45
+ end
data/lib/forger/cli.rb ADDED
@@ -0,0 +1,67 @@
1
+ module Forger
2
+ class CLI < Command
3
+ class_option :noop, type: :boolean
4
+ class_option :profile, desc: "profile name to use"
5
+
6
+ desc "clean SUBCOMMAND", "clean subcommands"
7
+ long_desc Help.text(:clean)
8
+ subcommand "clean", Clean
9
+
10
+ desc "wait SUBCOMMAND", "wait subcommands"
11
+ long_desc Help.text(:wait)
12
+ subcommand "wait", Wait
13
+
14
+ common_options = Proc.new do
15
+ option :auto_terminate, type: :boolean, default: false, desc: "automatically terminate the instance at the end of user-data"
16
+ option :cloudwatch, type: :boolean, default: false, desc: "enable cloudwatch logging, supported for amazonlinux2 and ubuntu"
17
+ end
18
+
19
+ desc "create NAME", "create ec2 instance"
20
+ long_desc Help.text(:create)
21
+ option :ami_name, desc: "when specified, an ami creation script is appended to the user-data script"
22
+ option :randomize, type: :boolean, desc: "append random characters to end of name"
23
+ option :source_ami, desc: "override the source image_id in profile"
24
+ common_options.call
25
+ def create(name)
26
+ Create.new(options.merge(name: name)).run
27
+ end
28
+
29
+ desc "ami NAME", "launches instance and uses it create AMI"
30
+ long_desc Help.text(:ami)
31
+ common_options.call
32
+ def ami(name)
33
+ Ami.new(options.merge(name: name)).run
34
+ end
35
+
36
+ desc "compile", "compiles app/scripts and app/user-data to tmp folder"
37
+ long_desc Help.text(:compile)
38
+ option :layout, default: "default", desc: "layout for user_data helper"
39
+ def compile
40
+ Script::Compile.new(options).compile_all
41
+ end
42
+
43
+ desc "upload", "compiles and uploads scripts to s3"
44
+ long_desc Help.text(:upload)
45
+ option :compile, type: :boolean, default: true, desc: "compile scripts before uploading"
46
+ def upload
47
+ Script::Upload.new(options).upload
48
+ end
49
+
50
+ desc "completion *PARAMS", "Prints words for auto-completion."
51
+ long_desc Help.text("completion")
52
+ def completion(*params)
53
+ Completer.new(CLI, *params).run
54
+ end
55
+
56
+ desc "completion_script", "Generates a script that can be eval to setup auto-completion."
57
+ long_desc Help.text("completion_script")
58
+ def completion_script
59
+ Completer::Script.generate
60
+ end
61
+
62
+ desc "version", "prints version"
63
+ def version
64
+ puts VERSION
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,67 @@
1
+ require "thor"
2
+
3
+ # Override thor's long_desc identation behavior
4
+ # https://github.com/erikhuda/thor/issues/398
5
+ class Thor
6
+ module Shell
7
+ class Basic
8
+ def print_wrapped(message, options = {})
9
+ message = "\n#{message}" unless message[0] == "\n"
10
+ stdout.puts message
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module Forger
17
+ class Command < Thor
18
+ class << self
19
+ def dispatch(m, args, options, config)
20
+ # Allow calling for help via:
21
+ # forger command help
22
+ # forger command -h
23
+ # forger command --help
24
+ # forger command -D
25
+ #
26
+ # as well thor's normal way:
27
+ #
28
+ # forger help command
29
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
30
+ if args.length > 1 && !(args & help_flags).empty?
31
+ args -= help_flags
32
+ args.insert(-2, "help")
33
+ end
34
+
35
+ # forger version
36
+ # forger --version
37
+ # forger -v
38
+ version_flags = ["--version", "-v"]
39
+ if args.length == 1 && !(args & version_flags).empty?
40
+ args = ["version"]
41
+ end
42
+
43
+ super
44
+ end
45
+
46
+ # Override command_help to include the description at the top of the
47
+ # long_description.
48
+ def command_help(shell, command_name)
49
+ meth = normalize_command_name(command_name)
50
+ command = all_commands[meth]
51
+ alter_command_description(command)
52
+ super
53
+ end
54
+
55
+ def alter_command_description(command)
56
+ return unless command
57
+ long_desc = if command.long_description
58
+ "#{command.description}\n\n#{command.long_description}"
59
+ else
60
+ command.description
61
+ end
62
+ command.long_description = long_desc
63
+ end
64
+ private :alter_command_description
65
+ end
66
+ end
67
+ end