forger 1.5.0

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.
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