forger 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|