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,106 @@
1
+ class Forger::Create
2
+ class Params < Forger::Base
3
+ # deep_symbolize_keys is ran at the very end only.
4
+ # up until that point we're dealing with String keys.
5
+ def generate
6
+ cleanup
7
+ params = Forger::Profile.new(@options).load
8
+ decorate_params(params)
9
+ normalize_launch_template(params).deep_symbolize_keys
10
+ end
11
+
12
+ def decorate_params(params)
13
+ upsert_name_tag!(params)
14
+ replace_runtime_options!(params)
15
+ params
16
+ end
17
+
18
+ # Expose a list of runtime params that are convenient. Try to limit the
19
+ # number of options from the cli to keep tool simple. Most options can
20
+ # be easily control through profile files. The runtime options that are
21
+ # very convenient to have at the CLI are modified here.
22
+ def replace_runtime_options!(params)
23
+ params["image_id"] = @options[:source_ami_id] if @options[:source_ami_id]
24
+ params
25
+ end
26
+
27
+ def cleanup
28
+ FileUtils.rm_f("#{Forger.root}/tmp/user-data.txt")
29
+ end
30
+
31
+ # Adds instance ec2 tag if not already provided
32
+ def upsert_name_tag!(params)
33
+ specs = params["tag_specifications"] || []
34
+
35
+ # insert an empty spec placeholder if one not found
36
+ spec = specs.find do |s|
37
+ s["resource_type"] == "instance"
38
+ end
39
+ unless spec
40
+ spec = {
41
+ "resource_type" => "instance",
42
+ "tags" => []
43
+ }
44
+ specs << spec
45
+ end
46
+ # guaranteed there's a tag_specifications with resource_type instance at this point
47
+
48
+ tags = spec["tags"] || []
49
+
50
+ unless tags.map { |t| t["key"] }.include?("Name")
51
+ tags << { "key" => "Name", "value" => @name }
52
+ end
53
+
54
+ specs = specs.map do |s|
55
+ # replace the name tag value
56
+ if s["resource_type"] == "instance"
57
+ {
58
+ "resource_type" => "instance",
59
+ "tags" => tags
60
+ }
61
+ else
62
+ s
63
+ end
64
+ end
65
+
66
+ params["tag_specifications"] = specs
67
+ params
68
+ end
69
+
70
+ # Allow adding launch template as a simple string.
71
+ #
72
+ # Standard structure:
73
+ # {
74
+ # launch_template: { launch_template_name: "TestLaunchTemplate" },
75
+ # }
76
+ #
77
+ # Simple string:
78
+ # {
79
+ # launch_template: "TestLaunchTemplate",
80
+ # }
81
+ #
82
+ # When launch_template is a simple String it will get transformed to the
83
+ # standard structure.
84
+ def normalize_launch_template(params)
85
+ if params["launch_template"].is_a?(String)
86
+ launch_template_identifier = params["launch_template"]
87
+ launch_template = if launch_template_identifier =~ /^lt-/
88
+ { "launch_template_id" => launch_template_identifier }
89
+ else
90
+ { "launch_template_name" => launch_template_identifier }
91
+ end
92
+ params["launch_template"] = launch_template
93
+ end
94
+ params
95
+ end
96
+
97
+ # Hard coded sensible defaults.
98
+ # Can be overridden easily with profiles
99
+ def defaults
100
+ {
101
+ max_count: 1,
102
+ min_count: 1,
103
+ }
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,30 @@
1
+ require 'dotenv'
2
+ require 'pathname'
3
+
4
+ class Forger::Dotenv
5
+ class << self
6
+ def load!
7
+ ::Dotenv.load(*dotenv_files)
8
+ end
9
+
10
+ # dotenv files will load the following files, starting from the bottom. The first value set (or those already defined in the environment) take precedence:
11
+
12
+ # - `.env` - The Original®
13
+ # - `.env.development`, `.env.test`, `.env.production` - Environment-specific settings.
14
+ # - `.env.local` - Local overrides. This file is loaded for all environments _except_ `test`.
15
+ # - `.env.development.local`, `.env.test.local`, `.env.production.local` - Local overrides of environment-specific settings.
16
+ #
17
+ def dotenv_files
18
+ [
19
+ root.join(".env.#{Forger.env}.local"),
20
+ (root.join(".env.local") unless Forger.env == "test"),
21
+ root.join(".env.#{Forger.env}"),
22
+ root.join(".env")
23
+ ].compact
24
+ end
25
+
26
+ def root
27
+ Forger.root || Pathname.new(ENV["AWS_EC2_ROOT"] || Dir.pwd)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module Forger::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ Examples:
2
+
3
+ $ forger ami myrubyami --profile ruby --noop
4
+
5
+ Launches an EC2 instance to create an AMI. An AMI creation script is appended to the end of the user-data script. The AMI creation script calls `aws ec2 create-image` and causes the instance to reboot at the end.
6
+
7
+ It is useful to include to timestamp as a part of the AMI name with the date command.
8
+
9
+ $ forger ami ruby-2.5.0_$(date "+%Y-%m-%d-%H-%M") --profile ruby --noop
10
+
11
+ The instance also automatically gets terminated and cleaned up by a termination script appended to user-data.
12
+
13
+ It is recommended to use the `set -e` option in your user-data script so that if the script fails, the AMI creation script is never reached and the instance is left behind so you can debug.
@@ -0,0 +1,22 @@
1
+ Examples:
2
+
3
+ $ forger clean ami 'base-amazonlinux2*'
4
+ $ forger clean ami 'base-ubuntu*' --keep 5
5
+ $ forger clean ami 'base-ubuntu*' --noop # dry-run
6
+
7
+ Deletes old AMIs using the provided name as the base portion of the AMI name to search for.
8
+
9
+ Let's say you have these images:
10
+
11
+ base-ubuntu_2018-03-25-04-20
12
+ base-ubuntu_2018-03-25-03-39
13
+ base-ubuntu_2018-03-25-02-57
14
+ base-ubuntu_2018-03-25-02-47
15
+ base-ubuntu_2018-03-25-02-43
16
+ base-ubuntu_2018-03-23-00-15
17
+
18
+ Running:
19
+
20
+ $ forger clean ami 'base-ubuntu*'
21
+
22
+ Would delete all images and keep the 2 most recent AMIs. The default `--keep` value is 2. Make sure to surround the query pattern with a single quote to prevent shell glob expansion.
@@ -0,0 +1,5 @@
1
+ Examples:
2
+
3
+ $ forger compile
4
+
5
+ Compiles app/scripts and app/user-data files to the tmp folder. Useful for inspection.
@@ -0,0 +1,22 @@
1
+ Example:
2
+
3
+ forger completion
4
+
5
+ Prints words for TAB auto-completion.
6
+
7
+ Examples:
8
+
9
+ forger completion
10
+ forger completion hello
11
+ forger completion hello name
12
+
13
+ To enable, TAB auto-completion add the following to your profile:
14
+
15
+ eval $(forger completion_script)
16
+
17
+ Auto-completion example usage:
18
+
19
+ forger [TAB]
20
+ forger hello [TAB]
21
+ forger hello name [TAB]
22
+ forger hello name --[TAB]
@@ -0,0 +1,3 @@
1
+ To use, add the following to your `~/.bashrc` or `~/.profile`
2
+
3
+ eval $(forger completion_script)
@@ -0,0 +1,7 @@
1
+ Examples:
2
+
3
+ $ forger create my-instance
4
+
5
+ To see the snippet of code that gets added to the user-data script you can use the `--noop` option and then view the generated tmp/user-data.txt.
6
+
7
+ $ forger create myscript --noop
@@ -0,0 +1,10 @@
1
+ Examples:
2
+
3
+ $ forger upload
4
+
5
+ Compiles the app/scripts and app/user-data files to the tmp folder. Then uploads the files to an s3 bucket that is configured in config/settings.yml. Example s3_folder setting:
6
+
7
+ ```yaml
8
+ development:
9
+ s3_folder: my-bucket/folder # enables auto sync to s3
10
+ ```
@@ -0,0 +1,12 @@
1
+ Examples:
2
+
3
+ $ forger wait ami ruby-2.5.0_2018-03-24-17-07
4
+ $ forger wait ami ami-b0138dc8
5
+
6
+ Polls the AMI with the given AMI name or id until AMI is found and available.
7
+
8
+ ### Timeout
9
+
10
+ Command times out after 30 mins by default. You can control the timeout with the `--timeout` flag. The timeout is specified in seconds.
11
+
12
+ $ forger wait ami --id ami-b0138dc8 --timeout 3600
@@ -0,0 +1,33 @@
1
+ require 'yaml'
2
+
3
+ module Forger
4
+ class Hook
5
+ def initialize(options={})
6
+ @options = options
7
+ end
8
+
9
+ def run(name)
10
+ return if @options[:noop]
11
+ return unless hooks[name]
12
+ command = hooks[name]
13
+ puts "Running hook #{name}: #{command}"
14
+ sh(command)
15
+ end
16
+
17
+ def hooks
18
+ hooks_path = "#{Forger.root}/config/hooks.yml"
19
+ data = File.exist?(hooks_path) ? YAML.load_file(hooks_path) : {}
20
+ data ? data : {} # in case the file is empty
21
+ end
22
+
23
+ def sh(command)
24
+ puts "=> #{command}".colorize(:cyan)
25
+ success = system(command)
26
+ abort("Command failed") unless success
27
+ end
28
+
29
+ def self.run(name, options={})
30
+ Hook.new(options).run(name.to_s)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ module Forger
2
+ class Profile < Base
3
+ include Forger::Template
4
+
5
+ def load
6
+ return @profile_params if @profile_params
7
+
8
+ check!
9
+
10
+ file = profile_file(profile_name)
11
+ @profile_params = load_profile(file)
12
+ end
13
+
14
+ def check!
15
+ file = profile_file(profile_name)
16
+ return if File.exist?(file)
17
+
18
+ puts "Unable to find a #{file.colorize(:green)} profile file."
19
+ puts "Please double check that it exists or that you specified the right profile.".colorize(:red)
20
+ exit 1
21
+ end
22
+
23
+ def load_profile(file)
24
+ return {} unless File.exist?(file)
25
+
26
+ puts "Using profile: #{file}".colorize(:green)
27
+ text = RenderMePretty.result(file, context: context)
28
+ begin
29
+ data = YAML.load(text)
30
+ rescue Psych::SyntaxError => e
31
+ tmp_file = file.sub("profiles", "tmp")
32
+ IO.write(tmp_file, text)
33
+ puts "There was an error evaluating in your yaml file #{file}".colorize(:red)
34
+ puts "The evaludated yaml file has been saved at #{tmp_file} for debugging."
35
+ puts "ERROR: #{e.message}"
36
+ exit 1
37
+ end
38
+ data ? data : {} # in case the file is empty
39
+ data.has_key?("run_instances") ? data["run_instances"] : data
40
+ end
41
+
42
+ # Determines a valid profile_name. Falls back to default
43
+ def profile_name
44
+ # allow user to specify the path also
45
+ if @options[:profile] && File.exist?(@options[:profile])
46
+ filename_profile = File.basename(@options[:profile], '.yml')
47
+ end
48
+
49
+ name = derandomize(@name)
50
+ if File.exist?(profile_file(name))
51
+ name_profile = name
52
+ end
53
+
54
+ filename_profile ||
55
+ @options[:profile] ||
56
+ name_profile || # conventional profile is the name of the ec2 instance
57
+ "default"
58
+ end
59
+
60
+ def profile_file(name)
61
+ "#{Forger.root}/profiles/#{name}.yml"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,46 @@
1
+ module Forger
2
+ class Script
3
+ autoload :Compile, "forger/script/compile"
4
+ autoload :Compress, "forger/script/compress"
5
+ autoload :Upload, "forger/script/upload"
6
+
7
+ def initialize(options={})
8
+ @options = options
9
+ end
10
+
11
+ def add_to_user_data!(user_data)
12
+ user_data
13
+ end
14
+
15
+ def auto_terminate_after_timeout
16
+ load_template("auto_terminate_after_timeout.sh")
17
+ end
18
+
19
+ def auto_terminate
20
+ # set variables for the template
21
+ @ami_name = @options[:ami_name]
22
+ load_template("auto_terminate.sh")
23
+ end
24
+
25
+ def cloudwatch
26
+ load_template("cloudwatch.sh")
27
+ end
28
+
29
+ def create_ami
30
+ # set variables for the template
31
+ @ami_name = @options[:ami_name]
32
+ @region = `aws configure get region`.strip rescue 'us-east-1'
33
+ load_template("ami_creation.sh")
34
+ end
35
+
36
+ def extract_forger_scripts
37
+ load_template("extract_forger_scripts.sh")
38
+ end
39
+
40
+ private
41
+ def load_template(name)
42
+ template = IO.read(File.expand_path("script/templates/#{name}", File.dirname(__FILE__)))
43
+ text = ERB.new(template, nil, "-").result(binding)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ require 'fileutils'
2
+
3
+ # Class for forger compile command
4
+ class Forger::Script
5
+ class Compile < Forger::Base
6
+ include Forger::Template
7
+
8
+ # used in upload
9
+ def compile_scripts
10
+ clean
11
+ compile_folder("scripts")
12
+ end
13
+
14
+ # use in compile cli command
15
+ def compile_all
16
+ clean
17
+ compile_folder("scripts")
18
+ layout_path = context.layout_path(@options[:layout])
19
+ compile_folder("user-data", layout_path)
20
+ end
21
+
22
+ def compile_folder(folder, layout_path=false)
23
+ puts "Compiling app/#{folder} to tmp/app/#{folder}.".colorize(:green)
24
+ Dir.glob("#{Forger.root}/app/#{folder}/**/*").each do |path|
25
+ next if File.directory?(path)
26
+ next if path.include?("layouts")
27
+
28
+ result = RenderMePretty.result(path, layout: layout_path, context: context)
29
+ tmp_path = path.sub(%r{.*/app/}, "#{BUILD_ROOT}/app/")
30
+ puts " #{tmp_path}" if @options[:verbose]
31
+ FileUtils.mkdir_p(File.dirname(tmp_path))
32
+ IO.write(tmp_path, result)
33
+ end
34
+ end
35
+
36
+ def clean
37
+ FileUtils.rm_rf("#{BUILD_ROOT}/app")
38
+ end
39
+ end
40
+ end