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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.gitmodules +0 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +147 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +136 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +249 -0
- data/Rakefile +6 -0
- data/docs/example/.env +2 -0
- data/docs/example/.env.development +2 -0
- data/docs/example/.env.production +3 -0
- data/docs/example/app/scripts/hello.sh +3 -0
- data/docs/example/app/user-data/bootstrap.sh +35 -0
- data/docs/example/config/development.yml +8 -0
- data/docs/example/profiles/default.yml +11 -0
- data/docs/example/profiles/spot.yml +20 -0
- data/exe/forger +14 -0
- data/forger.gemspec +38 -0
- data/lib/forger.rb +29 -0
- data/lib/forger/ami.rb +10 -0
- data/lib/forger/aws_service.rb +7 -0
- data/lib/forger/base.rb +42 -0
- data/lib/forger/clean.rb +13 -0
- data/lib/forger/cleaner.rb +5 -0
- data/lib/forger/cleaner/ami.rb +45 -0
- data/lib/forger/cli.rb +67 -0
- data/lib/forger/command.rb +67 -0
- data/lib/forger/completer.rb +161 -0
- data/lib/forger/completer/script.rb +6 -0
- data/lib/forger/completer/script.sh +10 -0
- data/lib/forger/config.rb +20 -0
- data/lib/forger/core.rb +51 -0
- data/lib/forger/create.rb +155 -0
- data/lib/forger/create/error_messages.rb +58 -0
- data/lib/forger/create/params.rb +106 -0
- data/lib/forger/dotenv.rb +30 -0
- data/lib/forger/help.rb +9 -0
- data/lib/forger/help/ami.md +13 -0
- data/lib/forger/help/clean/ami.md +22 -0
- data/lib/forger/help/compile.md +5 -0
- data/lib/forger/help/completion.md +22 -0
- data/lib/forger/help/completion_script.md +3 -0
- data/lib/forger/help/create.md +7 -0
- data/lib/forger/help/upload.md +10 -0
- data/lib/forger/help/wait/ami.md +12 -0
- data/lib/forger/hook.rb +33 -0
- data/lib/forger/profile.rb +64 -0
- data/lib/forger/script.rb +46 -0
- data/lib/forger/script/compile.rb +40 -0
- data/lib/forger/script/compress.rb +62 -0
- data/lib/forger/script/templates/ami_creation.sh +12 -0
- data/lib/forger/script/templates/auto_terminate.sh +11 -0
- data/lib/forger/script/templates/auto_terminate_after_timeout.sh +5 -0
- data/lib/forger/script/templates/cloudwatch.sh +3 -0
- data/lib/forger/script/templates/extract_aws_ec2_scripts.sh +48 -0
- data/lib/forger/script/upload.rb +99 -0
- data/lib/forger/scripts/auto_terminate.sh +14 -0
- data/lib/forger/scripts/auto_terminate/after_timeout.sh +18 -0
- data/lib/forger/scripts/auto_terminate/functions.sh +130 -0
- data/lib/forger/scripts/auto_terminate/functions/amazonlinux2.sh +10 -0
- data/lib/forger/scripts/auto_terminate/functions/ubuntu.sh +11 -0
- data/lib/forger/scripts/auto_terminate/setup.sh +31 -0
- data/lib/forger/scripts/cloudwatch.sh +24 -0
- data/lib/forger/scripts/cloudwatch/configure.sh +84 -0
- data/lib/forger/scripts/cloudwatch/install.sh +3 -0
- data/lib/forger/scripts/cloudwatch/install/amazonlinux2.sh +4 -0
- data/lib/forger/scripts/cloudwatch/install/ubuntu.sh +23 -0
- data/lib/forger/scripts/cloudwatch/service.sh +3 -0
- data/lib/forger/scripts/cloudwatch/service/amazonlinux2.sh +11 -0
- data/lib/forger/scripts/cloudwatch/service/ubuntu.sh +8 -0
- data/lib/forger/scripts/shared/functions.sh +78 -0
- data/lib/forger/setting.rb +52 -0
- data/lib/forger/template.rb +13 -0
- data/lib/forger/template/context.rb +32 -0
- data/lib/forger/template/helper.rb +17 -0
- data/lib/forger/template/helper/ami_helper.rb +33 -0
- data/lib/forger/template/helper/core_helper.rb +127 -0
- data/lib/forger/template/helper/partial_helper.rb +71 -0
- data/lib/forger/template/helper/script_helper.rb +53 -0
- data/lib/forger/template/helper/ssh_key_helper.rb +21 -0
- data/lib/forger/version.rb +3 -0
- data/lib/forger/wait.rb +12 -0
- data/lib/forger/waiter.rb +5 -0
- data/lib/forger/waiter/ami.rb +61 -0
- data/spec/fixtures/demo_project/config/settings.yml +22 -0
- data/spec/fixtures/demo_project/config/test.yml +9 -0
- data/spec/fixtures/demo_project/profiles/default.yml +33 -0
- data/spec/lib/cli_spec.rb +41 -0
- data/spec/lib/params_spec.rb +71 -0
- data/spec/spec_helper.rb +33 -0
- 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
|
data/lib/forger/help.rb
ADDED
@@ -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,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,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
|
data/lib/forger/hook.rb
ADDED
@@ -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
|