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
@@ -0,0 +1,71 @@
1
+ module Forger::Template::Helper::PartialHelper
2
+ def partial_exist?(path)
3
+ path = partial_path_for(path)
4
+ path = auto_add_format(path)
5
+ path && File.exist?(path)
6
+ end
7
+
8
+ # The partial's path is a relative path given without the extension and
9
+ #
10
+ # Example:
11
+ # Given: file in app/partials/mypartial.sh
12
+ # The path should be: mypartial
13
+ #
14
+ # If the user specifies the extension then use that instead of auto-adding
15
+ # the detected format.
16
+ def partial(path,vars={}, options={})
17
+ path = partial_path_for(path)
18
+ path = auto_add_format(path)
19
+
20
+ result = RenderMePretty.result(path, context: self)
21
+ result = indent(result, options[:indent]) if options[:indent]
22
+ if options[:indent]
23
+ # Add empty line at beginning because empty lines gets stripped during
24
+ # processing anyway. This allows the user to call partial without having
25
+ # to put the partial call at very beginning of the line.
26
+ # This only should happen if user is using indent option.
27
+ ["\n", result].join("\n")
28
+ else
29
+ result
30
+ end
31
+ end
32
+
33
+ # add indentation
34
+ def indent(text, indentation_amount)
35
+ text.split("\n").map do |line|
36
+ " " * indentation_amount + line
37
+ end.join("\n")
38
+ end
39
+
40
+ private
41
+ def partial_path_for(path)
42
+ "#{Forger.root}/app/partials/#{path}"
43
+ end
44
+
45
+ def auto_add_format(path)
46
+ # Return immediately if user provided explicit extension
47
+ extension = File.extname(path) # current extension
48
+ return path if !extension.empty?
49
+
50
+ # Else let's auto detect
51
+ paths = Dir.glob("#{path}.*")
52
+
53
+ if paths.size == 1 # non-ambiguous match
54
+ return paths.first
55
+ end
56
+
57
+ if paths.size > 1 # ambiguous match
58
+ puts "ERROR: Multiple possible partials found:".colorize(:red)
59
+ paths.each do |path|
60
+ puts " #{path}"
61
+ end
62
+ puts "Please specify an extension in the name to remove the ambiguity.".colorize(:green)
63
+ exit 1
64
+ end
65
+
66
+ # Account for case when user wants to include a file with no extension at all
67
+ return path if File.exist?(path) && !File.directory?(path)
68
+
69
+ path # original path if this point is reached
70
+ end
71
+ end
@@ -0,0 +1,53 @@
1
+ module Forger::Template::Helper::ScriptHelper
2
+ # Bash code that is meant to included in user-data
3
+ def extract_scripts(options={})
4
+ check_s3_folder_settings!
5
+
6
+ settings_options = settings["extract_scripts"] || {}
7
+ options = settings_options.merge(options)
8
+ # defaults also here in case they are removed from settings
9
+ to = options[:to] || "/opt"
10
+ user = options[:as] || "ec2-user"
11
+
12
+ if Dir.glob("#{Forger.root}/app/scripts*").empty?
13
+ puts "WARN: you are using the extract_scripts helper method but you do not have any app/scripts.".colorize(:yellow)
14
+ calling_line = caller[0].split(':')[0..1].join(':')
15
+ puts "Called from: #{calling_line}"
16
+ return ""
17
+ end
18
+
19
+ <<-BASH_CODE
20
+ # Generated from the forger extract_scripts helper.
21
+ # Downloads scripts from s3, extract them, and setup.
22
+ mkdir -p #{to}
23
+ aws s3 cp #{scripts_s3_path} #{to}/
24
+ (
25
+ cd #{to}
26
+ tar zxf #{to}/#{scripts_name}
27
+ chmod -R a+x #{to}/scripts
28
+ chown -R #{user}:#{user} #{to}/scripts
29
+ )
30
+ BASH_CODE
31
+ end
32
+
33
+ private
34
+ def check_s3_folder_settings!
35
+ return if settings["s3_folder"]
36
+
37
+ puts "Helper method called that requires the s3_folder to be set at:"
38
+ lines = caller.reject { |l| l =~ %r{lib/forger} } # hide internal forger trace
39
+ puts " #{lines[0]}"
40
+
41
+ puts "Please configure your config/settings.yml with an s3_folder.".colorize(:red)
42
+ exit 1
43
+ end
44
+
45
+ def scripts_name
46
+ File.basename(scripts_s3_path)
47
+ end
48
+
49
+ def scripts_s3_path
50
+ upload = Forger::Script::Upload.new
51
+ upload.s3_dest
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ module Forger::Template::Helper::SshKeyHelper
2
+ def add_ssh_key(user="ec2-user")
3
+ key_path = "#{ENV['HOME']}/.ssh/id_rsa.pub"
4
+ if File.exist?(key_path)
5
+ public_key = IO.read(key_path).strip
6
+ end
7
+ if public_key
8
+ <<-SCRIPT
9
+ # Automatically add user's public key from #{key_path}
10
+ cp /home/#{user}/.ssh/authorized_keys{,.bak}
11
+ echo #{public_key} >> /home/#{user}/.ssh/authorized_keys
12
+ chown #{user}:#{user} /home/#{user}/.ssh/authorized_keys
13
+ SCRIPT
14
+ else
15
+ <<-SCRIPT
16
+ # WARN: unable to find a ~/.ssh/id_rsa.pub locally on your machine. user: #{ENV['USER']}
17
+ # Unable to automatically add the public key
18
+ SCRIPT
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Forger
2
+ VERSION = "1.5.0"
3
+ end
@@ -0,0 +1,12 @@
1
+ module Forger
2
+ autoload :Waiter, 'forger/waiter'
3
+
4
+ class Wait < Command
5
+ desc "ami", "Wait until AMI available."
6
+ long_desc Help.text("wait:ami")
7
+ option :timeout, type: :numeric, default: 1800, desc: "Timeout in seconds."
8
+ def ami(name)
9
+ Waiter::Ami.new(options.merge(name: name)).wait
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module Forger
2
+ module Waiter
3
+ autoload :Ami, 'forger/waiter/ami'
4
+ end
5
+ end
@@ -0,0 +1,61 @@
1
+ module Forger::Waiter
2
+ class Ami < Forger::Base
3
+ include Forger::AwsService
4
+
5
+ def wait
6
+ delay = 30
7
+ timeout = @options[:timeout]
8
+ max_attempts = timeout / delay
9
+ current_time = 0
10
+
11
+ puts "Waiting for #{@options[:name]} to be available. Delay: #{delay}s. Timeout: #{timeout}s"
12
+ puts "Current time: #{Time.now}"
13
+ return if ENV['TEST']
14
+
15
+ # Using while loop because of issues with ruby's Timeout module
16
+ # http://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/
17
+ detected = detect_ami
18
+ until detected || current_time > timeout
19
+ print '.'
20
+ sleep delay
21
+ current_time += 30
22
+ detected = detect_ami
23
+ end
24
+ puts
25
+
26
+ if current_time > timeout
27
+ puts "ERROR: Timeout. Unable to detect and available ami: #{@options[:name]}"
28
+ exit 1
29
+ else
30
+ puts "Found available AMI: #{@options[:name]}"
31
+ end
32
+ end
33
+
34
+ private
35
+ # Using custom detect_ami instead of ec2.wait_until(:image_availalbe, ...)
36
+ # because we start checking for the ami even before we've called
37
+ # create_ami. We start checking right after we launch the instance
38
+ # which will create the ami at the end.
39
+ def detect_ami(owners=["self"])
40
+ images = ec2.describe_images(
41
+ owners: owners,
42
+ filters: filters
43
+ ).images
44
+ detected = images.first
45
+ !!detected
46
+ end
47
+
48
+ def filters
49
+ name_is_ami_id = @options[:name] =~ /^ami-/
50
+
51
+ filters = [{name: "state", values: ["available"]}]
52
+ filters << if name_is_ami_id
53
+ {name: "image-id", values: [@options[:name]]}
54
+ else
55
+ {name: "name", values: [@options[:name]]}
56
+ end
57
+
58
+ filters
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ ---
2
+ # Settings control internal forger behavior.
3
+ # Settings are different from the config files. The config files are meant to
4
+ # expose config variables that you can use in your ERB code.
5
+ # There are separate files to separate user defined variables and internal
6
+ # setting configs.
7
+ development:
8
+ # By setting s3_folder, forger will automatically tarball and upload your scripts
9
+ # to set. You then can then use the extract_scripts helper method to download
10
+ # the scripts onto the server.
11
+ # s3_folder: mybucket/path/to/folder # simple string
12
+ # compile_clean: true # uncomment to clean
13
+ # extract_scripts:
14
+ # to: "/opt"
15
+ # as: "ec2-user"
16
+ # aws_profiles:
17
+ # - profile1
18
+
19
+ production:
20
+ # s3_folder: mybucket/folder
21
+ # aws_profiles:
22
+ # - profile2
@@ -0,0 +1,9 @@
1
+ ---
2
+ vpc_id: vpc-123
3
+ db_subnet_group_name: default # default db subnet group associated with default vpc
4
+ subnets:
5
+ - subnet-123
6
+ - subnet-456
7
+ security_group_ids:
8
+ - sg-123 # test-single-instance
9
+ s3_bucket: my-bucket # for the user-data shared scripts
@@ -0,0 +1,33 @@
1
+ ---
2
+ # image_id: ami-97785bed # Amazon Linux AMI
3
+ image_id: ami-4fffc834 # Amazon Lambda AMI
4
+ # https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
5
+ instance_type: t2.medium
6
+ key_name: default
7
+ max_count: 1
8
+ min_count: 1
9
+ user_data:
10
+ iam_instance_profile:
11
+ name: DevLinux
12
+ # public network settings
13
+ security_group_ids: <%= config["security_group_ids"].inspect %>
14
+ subnet_id: <%= config["subnets"].shuffle.first %>
15
+ block_device_mappings:
16
+ - device_name: /dev/xvda
17
+ ebs:
18
+ volume_size: 20
19
+ instance_market_options:
20
+ market_type: spot
21
+ # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html
22
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html
23
+ spot_options:
24
+ max_price: "0.018" # $0.020/hr = $14.40/mo
25
+ # $0.018/hr = $12.96/mo
26
+ # valid combinations:
27
+ # spot_instance_type: persistent
28
+ # instance_interruption_behavior: hibernate
29
+ # or
30
+ # spot_instance_type: one-time
31
+ # More info: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html
32
+ spot_instance_type: persistent
33
+ instance_interruption_behavior: hibernate
@@ -0,0 +1,41 @@
1
+ describe Forger::CLI do
2
+ before(:all) do
3
+ @args = "--noop"
4
+ end
5
+
6
+ describe "forger" do
7
+ it "create" do
8
+ out = execute("exe/forger create server #{@args}")
9
+ expect(out).to include("Creating EC2 instance")
10
+ end
11
+
12
+ it "ami" do
13
+ out = execute("exe/forger ami myimage #{@args}")
14
+ expect(out).to include("Creating EC2 instance")
15
+ end
16
+
17
+ it "wait ami" do
18
+ out = execute("exe/forger wait ami myimage")
19
+ expect(out).to include("Waiting for")
20
+ end
21
+
22
+ it "clean ami" do
23
+ out = execute("exe/forger clean ami imagebasename")
24
+ expect(out).to include("Cleaning out old AMIs")
25
+ end
26
+
27
+ commands = {
28
+ "am" => "ami",
29
+ "compile" => "--profile",
30
+ "create -" => "--profile",
31
+ "create" => "name",
32
+ "create name --" => "--profile",
33
+ }
34
+ commands.each do |command, expected_word|
35
+ it "completion #{command}" do
36
+ out = execute("exe/forger completion #{command}")
37
+ expect(out).to include(expected_word) # only checking for one word for simplicity
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
1
+ describe Forger::Create::Params do
2
+ let(:param) { Forger::Create::Params.new(name: "myserver") }
3
+
4
+ context "completely empty" do
5
+ it '#upsert_name_tag!' do
6
+ params = {}
7
+ result = param.upsert_name_tag!(params)
8
+ # puts "params: #{params.inspect}" # uncomment to see and debug
9
+ expect(result).to eq(
10
+ {"tag_specifications"=>
11
+ [{"resource_type"=>"instance",
12
+ "tags"=>[{"key"=>"Name", "value"=>"myserver"}]}]}
13
+ )
14
+ end
15
+ end
16
+
17
+ context "empty tag_specifications" do
18
+ it '#upsert_name_tag!' do
19
+ params = {"tag_specifications" => []}
20
+ result = param.upsert_name_tag!(params)
21
+ # puts "params: #{params.inspect}" # uncomment to see and debug
22
+ expect(result).to eq(
23
+ {"tag_specifications"=>
24
+ [{"resource_type"=>"instance",
25
+ "tags"=>[{"key"=>"Name", "value"=>"myserver"}]}]}
26
+ )
27
+ end
28
+ end
29
+
30
+ context "contains 1 instance with name" do
31
+ it '#upsert_name_tag!' do
32
+ params = { "tag_specifications" =>
33
+ [{
34
+ "resource_type"=>"instance",
35
+ "tags"=> [{"key"=>"Name", "value"=>"override-myserver"} ]
36
+ }]
37
+ }
38
+ result = param.upsert_name_tag!(params)
39
+ # puts "params: #{params.inspect}" # uncomment to see and debug
40
+ expect(result).to eq(
41
+ {"tag_specifications"=>
42
+ [{"resource_type"=>"instance",
43
+ "tags"=>[{"key"=>"Name", "value"=>"override-myserver"}]}]}
44
+ )
45
+ end
46
+ end
47
+
48
+ context "contains 1 instance with non-name tag" do
49
+ it '#upsert_name_tag!' do
50
+ params = { "tag_specifications" =>
51
+ [{
52
+ "resource_type"=>"instance",
53
+ "tags"=> [{"key"=>"Os", "value"=>"amazonlinux"} ]
54
+ }]
55
+ }
56
+ result = param.upsert_name_tag!(params)
57
+ # puts "params: #{params.inspect}" # uncomment to see and debug
58
+ expect(result).to eq(
59
+ { "tag_specifications" =>
60
+ [{
61
+ "resource_type"=>"instance",
62
+ "tags"=> [
63
+ {"key"=>"Os", "value"=>"amazonlinux"},
64
+ {"key"=>"Name", "value"=>"myserver"},
65
+ ]
66
+ }]
67
+ }
68
+ )
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,33 @@
1
+ ENV["TEST"] = "1"
2
+ ENV["AWS_EC2_ENV"] = "test"
3
+ ENV["AWS_EC2_ROOT"] = "spec/fixtures/demo_project"
4
+ # Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentails
5
+ ENV['HOME'] = "spec/fixtures/home"
6
+
7
+ # CodeClimate test coverage: https://docs.codeclimate.com/docs/configuring-test-coverage
8
+ # require 'simplecov'
9
+ # SimpleCov.start
10
+
11
+ require "pp"
12
+ require "byebug"
13
+ root = File.expand_path("../", File.dirname(__FILE__))
14
+ require "#{root}/lib/forger"
15
+
16
+ module Helper
17
+ def execute(cmd)
18
+ puts "Running: #{cmd}" if show_command?
19
+ out = `#{cmd}`
20
+ puts out if show_command?
21
+ out
22
+ end
23
+
24
+ # Added SHOW_COMMAND because DEBUG is also used by other libraries like
25
+ # bundler and it shows its internal debugging logging also.
26
+ def show_command?
27
+ ENV['DEBUG'] || ENV['SHOW_COMMAND']
28
+ end
29
+ end
30
+
31
+ RSpec.configure do |c|
32
+ c.include Helper
33
+ end