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