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
data/Rakefile
ADDED
data/docs/example/.env
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/bin/bash -exu
|
2
|
+
|
3
|
+
export HOME=/root # user-data env runs in weird shell where user is root but HOME is not set
|
4
|
+
|
5
|
+
sudo yum install -y postgresql
|
6
|
+
|
7
|
+
# https://gist.github.com/juno/1330165
|
8
|
+
# Install developer tools
|
9
|
+
yum install -y git gcc make readline-devel openssl-devel
|
10
|
+
|
11
|
+
# Install ruby-build system-widely
|
12
|
+
git clone git://github.com/sstephenson/ruby-build.git /tmp/ruby-build
|
13
|
+
cd /tmp/ruby-build
|
14
|
+
./install.sh
|
15
|
+
echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc
|
16
|
+
|
17
|
+
# Install rbenv for root
|
18
|
+
git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
|
19
|
+
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
|
20
|
+
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
21
|
+
set +u
|
22
|
+
source ~/.bashrc
|
23
|
+
set -u
|
24
|
+
|
25
|
+
# Install and enable ruby
|
26
|
+
rbenv install 2.5.0
|
27
|
+
|
28
|
+
# Install ruby for ec2-user also
|
29
|
+
cp -R ~/.rbenv /home/ec2-user/
|
30
|
+
chown -R ec2-user:ec2-user /home/ec2-user/.rbenv
|
31
|
+
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> /home/ec2-user/.bashrc
|
32
|
+
echo 'eval "$(rbenv init -)"' >> /home/ec2-user/.bashrc
|
33
|
+
echo '2.5.0' > /home/ec2-user/.ruby-version
|
34
|
+
|
35
|
+
uptime | tee /var/log/boot-time.log
|
@@ -0,0 +1,11 @@
|
|
1
|
+
---
|
2
|
+
image_id: ami-97785bed
|
3
|
+
instance_type: t2.medium
|
4
|
+
key_name: default
|
5
|
+
max_count: 1
|
6
|
+
min_count: 1
|
7
|
+
security_group_ids: <%= config["security_group_ids"] %>
|
8
|
+
subnet_id: <%= config["subnets"].shuffle.first %>
|
9
|
+
user_data: "<%= user_data("bootstrap") %>"
|
10
|
+
iam_instance_profile:
|
11
|
+
name: IAMProfileName
|
@@ -0,0 +1,20 @@
|
|
1
|
+
---
|
2
|
+
image_id: ami-97785bed
|
3
|
+
instance_type: t2.medium
|
4
|
+
key_name: default
|
5
|
+
max_count: 1
|
6
|
+
min_count: 1
|
7
|
+
security_group_ids:
|
8
|
+
- sg-111
|
9
|
+
subnet_id: <%= %w[subnet-111 subnet-222].shuffle.first %>
|
10
|
+
user_data: "<%= user_data("dev") %>"
|
11
|
+
iam_instance_profile:
|
12
|
+
name: IAMProfileName
|
13
|
+
instance_market_options:
|
14
|
+
market_type: spot
|
15
|
+
# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html
|
16
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html
|
17
|
+
spot_options:
|
18
|
+
max_price: "0.02"
|
19
|
+
spot_instance_type: one-time
|
20
|
+
# instance_interruption_behavior: hibernate
|
data/exe/forger
ADDED
data/forger.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "forger/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "forger"
|
8
|
+
spec.version = Forger::VERSION
|
9
|
+
spec.authors = ["Tung Nguyen"]
|
10
|
+
spec.email = ["tongueroo@gmail.com"]
|
11
|
+
spec.description = %q{Simple tool to create AWS ec2 instances consistently with pre-configured settings}
|
12
|
+
spec.summary = %q{Simple tool to create AWS ec2 instances}
|
13
|
+
spec.homepage = "https://github.com/tongueroo/forger"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "activesupport"
|
23
|
+
spec.add_dependency "aws-sdk-ec2"
|
24
|
+
spec.add_dependency "aws-sdk-s3"
|
25
|
+
spec.add_dependency "colorize"
|
26
|
+
spec.add_dependency "dotenv"
|
27
|
+
spec.add_dependency "filesize"
|
28
|
+
spec.add_dependency "hashie"
|
29
|
+
spec.add_dependency "render_me_pretty"
|
30
|
+
spec.add_dependency "thor"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler"
|
33
|
+
spec.add_development_dependency "byebug"
|
34
|
+
spec.add_development_dependency "guard"
|
35
|
+
spec.add_development_dependency "guard-bundler"
|
36
|
+
spec.add_development_dependency "guard-rspec"
|
37
|
+
spec.add_development_dependency "rake"
|
38
|
+
end
|
data/lib/forger.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
$:.unshift(File.expand_path("../", __FILE__))
|
2
|
+
require "forger/version"
|
3
|
+
require "colorize"
|
4
|
+
require "render_me_pretty"
|
5
|
+
|
6
|
+
module Forger
|
7
|
+
autoload :Help, "forger/help"
|
8
|
+
autoload :Command, "forger/command"
|
9
|
+
autoload :CLI, "forger/cli"
|
10
|
+
autoload :AwsService, "forger/aws_service"
|
11
|
+
autoload :Profile, "forger/profile"
|
12
|
+
autoload :Base, "forger/base"
|
13
|
+
autoload :Create, "forger/create"
|
14
|
+
autoload :Ami, "forger/ami"
|
15
|
+
autoload :Wait, "forger/wait"
|
16
|
+
autoload :Clean, "forger/clean"
|
17
|
+
autoload :Template, "forger/template"
|
18
|
+
autoload :Script, "forger/script"
|
19
|
+
autoload :Config, "forger/config"
|
20
|
+
autoload :Core, "forger/core"
|
21
|
+
autoload :Dotenv, "forger/dotenv"
|
22
|
+
autoload :Hook, "forger/hook"
|
23
|
+
autoload :Completion, "forger/completion"
|
24
|
+
autoload :Completer, "forger/completer"
|
25
|
+
autoload :Setting, "forger/setting"
|
26
|
+
extend Core
|
27
|
+
end
|
28
|
+
|
29
|
+
Forger::Dotenv.load!
|
data/lib/forger/ami.rb
ADDED
data/lib/forger/base.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Forger
|
2
|
+
class Base
|
3
|
+
# constants really only used by script classes
|
4
|
+
SCRIPTS_INFO_PATH = "tmp/data/scripts_info.txt"
|
5
|
+
BUILD_ROOT = "tmp"
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@options = options.clone
|
9
|
+
@name = randomize(@options[:name])
|
10
|
+
Forger.validate_in_project!
|
11
|
+
end
|
12
|
+
|
13
|
+
# Appends a short random string at the end of the ec2 instance name.
|
14
|
+
# Later we will strip this same random string from the name.
|
15
|
+
# Very makes it convenient. We can just type:
|
16
|
+
#
|
17
|
+
# forger create server --randomize
|
18
|
+
#
|
19
|
+
# instead of:
|
20
|
+
#
|
21
|
+
# forger create server-123 --profile server
|
22
|
+
#
|
23
|
+
def randomize(name)
|
24
|
+
if @options[:randomize]
|
25
|
+
random = (0...3).map { (65 + rand(26)).chr }.join.downcase # Ex: jhx
|
26
|
+
[name, random].join('-')
|
27
|
+
else
|
28
|
+
name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Strip the random string at end of the ec2 instance name
|
33
|
+
def derandomize(name)
|
34
|
+
if @options[:randomize]
|
35
|
+
name.sub(/-(\w{3})$/,'') # strip the random part at the end
|
36
|
+
else
|
37
|
+
name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/forger/clean.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Forger
|
2
|
+
autoload :Cleaner, 'forger/cleaner'
|
3
|
+
|
4
|
+
class Clean < Command
|
5
|
+
desc "ami", "Clean until AMI available."
|
6
|
+
long_desc Help.text("clean:ami")
|
7
|
+
option :keep, type: :numeric, default: 2, desc: "Number of images to keep"
|
8
|
+
option :noop, type: :boolean, desc: "Noop or dry-run mode"
|
9
|
+
def ami(query)
|
10
|
+
Cleaner::Ami.new(options.merge(query: query)).clean
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Forger::Cleaner
|
2
|
+
class Ami < Forger::Base
|
3
|
+
include Forger::AwsService
|
4
|
+
|
5
|
+
def clean
|
6
|
+
query = @options[:query]
|
7
|
+
keep = @options[:keep] || 2
|
8
|
+
puts "Cleaning out old AMIs with base name: #{@options[:query]}"
|
9
|
+
return if ENV['TEST']
|
10
|
+
|
11
|
+
images = search_ami(query)
|
12
|
+
images = images.sort_by { |i| i.name }.reverse
|
13
|
+
delete_list = images[keep..-1] || []
|
14
|
+
puts "Deleting #{delete_list.size} images."
|
15
|
+
delete_list.each do |i|
|
16
|
+
delete(i)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def delete(image)
|
22
|
+
message = "Deleting image: #{image.image_id} #{image.name}"
|
23
|
+
if @options[:noop]
|
24
|
+
puts "NOOP: #{message}"
|
25
|
+
else
|
26
|
+
puts message
|
27
|
+
ec2.deregister_image(image_id: image.image_id)
|
28
|
+
end
|
29
|
+
rescue Aws::EC2::Errors::InvalidAMIIDUnavailable
|
30
|
+
# happens when image was just deleted but its still
|
31
|
+
# showing up as available when calling describe_images
|
32
|
+
puts "WARN: #{e.message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def search_ami(query, owners=["self"])
|
36
|
+
ec2.describe_images(
|
37
|
+
owners: owners,
|
38
|
+
filters: [
|
39
|
+
{name: "name", values: [query]},
|
40
|
+
{name: "state", values: ["available"]}
|
41
|
+
]
|
42
|
+
).images
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/forger/cli.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Forger
|
2
|
+
class CLI < Command
|
3
|
+
class_option :noop, type: :boolean
|
4
|
+
class_option :profile, desc: "profile name to use"
|
5
|
+
|
6
|
+
desc "clean SUBCOMMAND", "clean subcommands"
|
7
|
+
long_desc Help.text(:clean)
|
8
|
+
subcommand "clean", Clean
|
9
|
+
|
10
|
+
desc "wait SUBCOMMAND", "wait subcommands"
|
11
|
+
long_desc Help.text(:wait)
|
12
|
+
subcommand "wait", Wait
|
13
|
+
|
14
|
+
common_options = Proc.new do
|
15
|
+
option :auto_terminate, type: :boolean, default: false, desc: "automatically terminate the instance at the end of user-data"
|
16
|
+
option :cloudwatch, type: :boolean, default: false, desc: "enable cloudwatch logging, supported for amazonlinux2 and ubuntu"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "create NAME", "create ec2 instance"
|
20
|
+
long_desc Help.text(:create)
|
21
|
+
option :ami_name, desc: "when specified, an ami creation script is appended to the user-data script"
|
22
|
+
option :randomize, type: :boolean, desc: "append random characters to end of name"
|
23
|
+
option :source_ami, desc: "override the source image_id in profile"
|
24
|
+
common_options.call
|
25
|
+
def create(name)
|
26
|
+
Create.new(options.merge(name: name)).run
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "ami NAME", "launches instance and uses it create AMI"
|
30
|
+
long_desc Help.text(:ami)
|
31
|
+
common_options.call
|
32
|
+
def ami(name)
|
33
|
+
Ami.new(options.merge(name: name)).run
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "compile", "compiles app/scripts and app/user-data to tmp folder"
|
37
|
+
long_desc Help.text(:compile)
|
38
|
+
option :layout, default: "default", desc: "layout for user_data helper"
|
39
|
+
def compile
|
40
|
+
Script::Compile.new(options).compile_all
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "upload", "compiles and uploads scripts to s3"
|
44
|
+
long_desc Help.text(:upload)
|
45
|
+
option :compile, type: :boolean, default: true, desc: "compile scripts before uploading"
|
46
|
+
def upload
|
47
|
+
Script::Upload.new(options).upload
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "completion *PARAMS", "Prints words for auto-completion."
|
51
|
+
long_desc Help.text("completion")
|
52
|
+
def completion(*params)
|
53
|
+
Completer.new(CLI, *params).run
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "completion_script", "Generates a script that can be eval to setup auto-completion."
|
57
|
+
long_desc Help.text("completion_script")
|
58
|
+
def completion_script
|
59
|
+
Completer::Script.generate
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "version", "prints version"
|
63
|
+
def version
|
64
|
+
puts VERSION
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
# Override thor's long_desc identation behavior
|
4
|
+
# https://github.com/erikhuda/thor/issues/398
|
5
|
+
class Thor
|
6
|
+
module Shell
|
7
|
+
class Basic
|
8
|
+
def print_wrapped(message, options = {})
|
9
|
+
message = "\n#{message}" unless message[0] == "\n"
|
10
|
+
stdout.puts message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Forger
|
17
|
+
class Command < Thor
|
18
|
+
class << self
|
19
|
+
def dispatch(m, args, options, config)
|
20
|
+
# Allow calling for help via:
|
21
|
+
# forger command help
|
22
|
+
# forger command -h
|
23
|
+
# forger command --help
|
24
|
+
# forger command -D
|
25
|
+
#
|
26
|
+
# as well thor's normal way:
|
27
|
+
#
|
28
|
+
# forger help command
|
29
|
+
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
30
|
+
if args.length > 1 && !(args & help_flags).empty?
|
31
|
+
args -= help_flags
|
32
|
+
args.insert(-2, "help")
|
33
|
+
end
|
34
|
+
|
35
|
+
# forger version
|
36
|
+
# forger --version
|
37
|
+
# forger -v
|
38
|
+
version_flags = ["--version", "-v"]
|
39
|
+
if args.length == 1 && !(args & version_flags).empty?
|
40
|
+
args = ["version"]
|
41
|
+
end
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# Override command_help to include the description at the top of the
|
47
|
+
# long_description.
|
48
|
+
def command_help(shell, command_name)
|
49
|
+
meth = normalize_command_name(command_name)
|
50
|
+
command = all_commands[meth]
|
51
|
+
alter_command_description(command)
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def alter_command_description(command)
|
56
|
+
return unless command
|
57
|
+
long_desc = if command.long_description
|
58
|
+
"#{command.description}\n\n#{command.long_description}"
|
59
|
+
else
|
60
|
+
command.description
|
61
|
+
end
|
62
|
+
command.long_description = long_desc
|
63
|
+
end
|
64
|
+
private :alter_command_description
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|