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