forger 2.0.5 → 3.0.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile.lock +40 -29
  5. data/README.md +6 -37
  6. data/docs/example/config/variables/development.rb +2 -0
  7. data/docs/example/profiles/default.yml +2 -2
  8. data/docs/extract-scripts.md +40 -0
  9. data/docs/layouts.md +35 -0
  10. data/docs/profiles.md +79 -0
  11. data/docs/variables.md +53 -0
  12. data/forger.gemspec +6 -3
  13. data/lib/forger.rb +4 -23
  14. data/lib/forger/autoloader.rb +21 -0
  15. data/lib/forger/aws_services.rb +22 -0
  16. data/lib/forger/clean.rb +0 -2
  17. data/lib/forger/cleaner.rb +0 -1
  18. data/lib/forger/cleaner/ami.rb +1 -1
  19. data/lib/forger/cli.rb +10 -6
  20. data/lib/forger/completer.rb +0 -2
  21. data/lib/forger/core.rb +28 -12
  22. data/lib/forger/create.rb +2 -23
  23. data/lib/forger/create/info.rb +10 -4
  24. data/lib/forger/create/waiter.rb +1 -1
  25. data/lib/forger/destroy.rb +1 -1
  26. data/lib/forger/help/upload.md +1 -13
  27. data/lib/forger/network.rb +2 -2
  28. data/lib/forger/new.rb +5 -6
  29. data/lib/forger/profile.rb +15 -3
  30. data/lib/forger/s3.rb +23 -0
  31. data/lib/forger/s3/bucket.rb +131 -0
  32. data/lib/forger/script.rb +0 -4
  33. data/lib/forger/script/upload.rb +12 -42
  34. data/lib/forger/scripts/cloudwatch.sh +2 -2
  35. data/lib/forger/scripts/shared/functions.sh +1 -1
  36. data/lib/forger/setting.rb +0 -32
  37. data/lib/forger/template.rb +0 -3
  38. data/lib/forger/template/context.rb +16 -1
  39. data/lib/forger/template/helper.rb +9 -9
  40. data/lib/forger/template/helper/ami_helper.rb +1 -1
  41. data/lib/forger/template/helper/core_helper.rb +7 -6
  42. data/lib/forger/template/helper/script_helper.rb +3 -9
  43. data/lib/forger/version.rb +1 -1
  44. data/lib/forger/wait.rb +0 -2
  45. data/lib/forger/waiter.rb +0 -1
  46. data/lib/forger/waiter/ami.rb +1 -1
  47. data/lib/templates/default/app/user_data/bootstrap.sh.tt +6 -9
  48. data/lib/templates/default/app/user_data/layouts/default.sh.tt +1 -5
  49. data/lib/templates/default/config/settings.yml.tt +1 -11
  50. data/lib/templates/default/config/{development.yml.tt → variables/development.yml.tt} +0 -0
  51. data/spec/fixtures/demo_project/app/user_data/bootstrap.sh +2 -2
  52. data/spec/fixtures/demo_project/config/settings.yml +2 -4
  53. data/spec/fixtures/demo_project/config/variables/test.rb +4 -0
  54. data/spec/fixtures/demo_project/profiles/default.yml +2 -2
  55. metadata +59 -13
  56. data/docs/example/config/development.yml +0 -7
  57. data/lib/forger/aws_service.rb +0 -7
  58. data/lib/forger/config.rb +0 -25
  59. data/spec/fixtures/demo_project/config/test.yml +0 -8
@@ -6,9 +6,9 @@ require "forger/version"
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "forger"
8
8
  spec.version = Forger::VERSION
9
- spec.authors = ["Tung Nguyen"]
10
- spec.email = ["tongueroo@gmail.com"]
11
- spec.summary = "Tool to create AWS ec2 instances"
9
+ spec.author = "Tung Nguyen"
10
+ spec.email = "tongueroo@gmail.com"
11
+ spec.summary = "Create EC2 Instances with preconfigured settings"
12
12
  spec.homepage = "https://github.com/tongueroo/forger"
13
13
  spec.license = "MIT"
14
14
 
@@ -19,8 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "activesupport"
22
+ spec.add_dependency "aws-sdk-cloudformation"
22
23
  spec.add_dependency "aws-sdk-ec2"
23
24
  spec.add_dependency "aws-sdk-s3"
25
+ spec.add_dependency "cfn-status"
24
26
  spec.add_dependency "dotenv"
25
27
  spec.add_dependency "filesize"
26
28
  spec.add_dependency "hashie"
@@ -28,6 +30,7 @@ Gem::Specification.new do |spec|
28
30
  spec.add_dependency "rainbow"
29
31
  spec.add_dependency "render_me_pretty"
30
32
  spec.add_dependency "thor"
33
+ spec.add_dependency "zeitwerk"
31
34
 
32
35
  spec.add_development_dependency "bundler"
33
36
  spec.add_development_dependency "byebug"
@@ -4,31 +4,12 @@ require "rainbow/ext/string"
4
4
  require "render_me_pretty"
5
5
  require "memoist"
6
6
 
7
+ require "forger/autoloader"
8
+ Forger::Autoloader.setup
9
+
7
10
  module Forger
8
- autoload :Ami, "forger/ami"
9
- autoload :AwsService, "forger/aws_service"
10
- autoload :Base, "forger/base"
11
- autoload :Clean, "forger/clean"
12
- autoload :CLI, "forger/cli"
13
- autoload :Command, "forger/command"
14
- autoload :Completer, "forger/completer"
15
- autoload :Completion, "forger/completion"
16
- autoload :Config, "forger/config"
17
- autoload :Core, "forger/core"
18
- autoload :Create, "forger/create"
19
- autoload :Destroy, "forger/destroy"
20
- autoload :Dotenv, "forger/dotenv"
21
- autoload :Help, "forger/help"
22
- autoload :Hook, "forger/hook"
23
- autoload :Network, "forger/network"
24
- autoload :New, "forger/new"
25
- autoload :Profile, "forger/profile"
26
- autoload :Script, "forger/script"
27
- autoload :Sequence, "forger/sequence"
28
- autoload :Setting, "forger/setting"
29
- autoload :Template, "forger/template"
30
- autoload :Wait, "forger/wait"
31
11
  extend Core
32
12
  end
33
13
 
34
14
  Forger::Dotenv.load!
15
+ Forger.set_aws_profile!
@@ -0,0 +1,21 @@
1
+ require "zeitwerk"
2
+
3
+ module Forger
4
+ class Autoloader
5
+ class Inflector < Zeitwerk::Inflector
6
+ def camelize(basename, _abspath)
7
+ map = { cli: "CLI", version: "VERSION" }
8
+ map[basename.to_sym] || super
9
+ end
10
+ end
11
+
12
+ class << self
13
+ def setup
14
+ loader = Zeitwerk::Loader.new
15
+ loader.inflector = Inflector.new
16
+ loader.push_dir(File.dirname(__dir__)) # lib
17
+ loader.setup
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ require 'aws-sdk-cloudformation'
2
+ require 'aws-sdk-ec2'
3
+ require 'aws-sdk-s3'
4
+
5
+ module Forger::AwsServices
6
+ extend Memoist
7
+
8
+ def cfn
9
+ Aws::CloudFormation::Client.new
10
+ end
11
+ memoize :cfn
12
+
13
+ def ec2
14
+ Aws::EC2::Client.new
15
+ end
16
+ memoize :ec2
17
+
18
+ def s3
19
+ Aws::S3::Client.new
20
+ end
21
+ memoize :s3
22
+ end
@@ -1,6 +1,4 @@
1
1
  module Forger
2
- autoload :Cleaner, 'forger/cleaner'
3
-
4
2
  class Clean < Command
5
3
  desc "ami", "Clean until AMI available."
6
4
  long_desc Help.text("clean:ami")
@@ -1,5 +1,4 @@
1
1
  module Forger
2
2
  module Cleaner
3
- autoload :Ami, 'forger/cleaner/ami'
4
3
  end
5
4
  end
@@ -1,6 +1,6 @@
1
1
  module Forger::Cleaner
2
2
  class Ami < Forger::Base
3
- include Forger::AwsService
3
+ include Forger::AwsServices
4
4
 
5
5
  def clean
6
6
  query = @options[:query]
@@ -14,6 +14,12 @@ module Forger
14
14
  common_options = Proc.new do
15
15
  option :auto_terminate, type: :boolean, default: false, desc: "automatically terminate the instance at the end of user-data"
16
16
  option :cloudwatch, type: :boolean, desc: "enable cloudwatch logging, supported for amazonlinux2 and ubuntu"
17
+ option :ami_name, desc: "when specified, an ami creation script is appended to the user-data script"
18
+ option :randomize, type: :boolean, desc: "append random characters to end of name"
19
+ option :source_ami, desc: "override the source image_id in profile"
20
+ option :wait, type: :boolean, default: true, desc: "Wait until the instance is ready and report dns name"
21
+ option :ssh, type: :boolean, desc: "Ssh into instance immediately after it's ready"
22
+ option :ssh_user, default: "ec2-user", desc: "User to use to with the ssh option to log into instance"
17
23
  end
18
24
 
19
25
  long_desc Help.text(:new)
@@ -24,12 +30,6 @@ module Forger
24
30
 
25
31
  desc "create NAME", "create ec2 instance"
26
32
  long_desc Help.text(:create)
27
- option :ami_name, desc: "when specified, an ami creation script is appended to the user-data script"
28
- option :randomize, type: :boolean, desc: "append random characters to end of name"
29
- option :source_ami, desc: "override the source image_id in profile"
30
- option :wait, type: :boolean, default: true, desc: "Wait until the instance is ready and report dns name"
31
- option :ssh, type: :boolean, desc: "Ssh into instance immediately after it's ready"
32
- option :ssh_user, default: "ec2-user", desc: "User to use to with the ssh option to log into instance"
33
33
  common_options.call
34
34
  def create(name)
35
35
  Create.new(options.merge(name: name)).run
@@ -78,5 +78,9 @@ module Forger
78
78
  def version
79
79
  puts VERSION
80
80
  end
81
+
82
+ desc "s3 SUBCOMMAND", "s3 subcommands"
83
+ long_desc Help.text(:s3)
84
+ subcommand "s3", S3
81
85
  end
82
86
  end
@@ -70,8 +70,6 @@ Auto-completion accounts for each of these type of commands.
70
70
  =end
71
71
  module Forger
72
72
  class Completer
73
- autoload :Script, 'forger/completer/script'
74
-
75
73
  def initialize(command_class, *params)
76
74
  @params = params
77
75
  @current_command = @params[0]
@@ -3,10 +3,7 @@ require 'yaml'
3
3
 
4
4
  module Forger
5
5
  module Core
6
- @@config = nil
7
- def config
8
- @@config ||= Config.new.data
9
- end
6
+ extend Memoist
10
7
 
11
8
  def settings
12
9
  Setting.new.data
@@ -44,18 +41,37 @@ module Forger
44
41
  Base::BUILD_ROOT
45
42
  end
46
43
 
44
+ # Overrides AWS_PROFILE based on the Forger.env if set in config/settings.yml
45
+ # 2-way binding.
46
+ def set_aws_profile!
47
+ return if ENV['TEST']
48
+ return unless File.exist?("#{Forger.root}/config/settings.yml") # for rake docs
49
+ return unless settings # Only load if within Ufo project and there's a settings.yml
50
+ data = settings[Forger.env] || {}
51
+ if data["aws_profile"]
52
+ puts "Using AWS_PROFILE=#{data["aws_profile"]} from FORGER_ENV=#{Forger.env} in config/settings.yml"
53
+ ENV['AWS_PROFILE'] = data["aws_profile"]
54
+ end
55
+ end
56
+
57
+ # Do not use the Setting#data to load the profile because it can cause an
58
+ # infinite loop then if we decide to use Forger.env from within settings class.
59
+ def settings
60
+ path = "#{Forger.root}/config/settings.yml"
61
+ return {} unless File.exist?(path)
62
+ YAML.load_file(path)
63
+ end
64
+ memoize :settings
65
+
47
66
  private
48
67
  # Do not use the Setting class to load the profile because it can cause an
49
68
  # infinite loop then if we decide to use Forger.env from within settings class.
50
69
  def env_from_profile(aws_profile)
51
- settings_path = "#{Forger.root}/config/settings.yml"
52
- return unless File.exist?(settings_path)
53
-
54
- data = YAML.load_file(settings_path)
55
- env = data.find do |_env, setting|
56
- setting ||= {}
57
- profiles = setting['aws_profiles']
58
- profiles && profiles.include?(aws_profile)
70
+ return unless settings
71
+ env = settings.find do |_env, settings|
72
+ settings ||= {}
73
+ profiles = settings['aws_profile']
74
+ profiles && profiles == aws_profile
59
75
  end
60
76
  env.first if env
61
77
  end
@@ -3,12 +3,7 @@ require 'active_support/core_ext/hash'
3
3
 
4
4
  module Forger
5
5
  class Create < Base
6
- autoload :Params, "forger/create/params"
7
- autoload :ErrorMessages, "forger/create/error_messages"
8
- autoload :Waiter, "forger/create/waiter"
9
- autoload :Info, "forger/create/info"
10
-
11
- include AwsService
6
+ include AwsServices
12
7
  include ErrorMessages
13
8
 
14
9
  def run
@@ -28,7 +23,7 @@ module Forger
28
23
 
29
24
  instance_id = resp.instances.first.instance_id
30
25
  info.spot(instance_id)
31
- puts "EC2 instance #{@name} created: #{instance_id} 🎉"
26
+ puts "EC2 instance with profile #{@name.color(:green)} created: #{instance_id} 🎉"
32
27
  puts "Visit https://console.aws.amazon.com/ec2/home to check on the status"
33
28
  info.cloudwatch(instance_id)
34
29
 
@@ -41,23 +36,7 @@ module Forger
41
36
  handle_ec2_service_error!(e)
42
37
  end
43
38
 
44
- # Configured by config/settings.yml.
45
- # Example: config/settings.yml:
46
- #
47
- # Format 1: Simple String
48
- #
49
- # development:
50
- # s3_folder: mybucket/path/to/folder
51
- #
52
- # Format 2: Hash
53
- #
54
- # development:
55
- # s3_folder:
56
- # default: mybucket/path/to/folder
57
- # dev_profile1: mybucket/path/to/folder
58
- # dev_profile1: another-bucket/storage/path
59
39
  def sync_scripts_to_s3
60
- return unless Forger.settings["s3_folder"]
61
40
  upload = Script::Upload.new(@options)
62
41
  return if upload.empty?
63
42
  upload.run
@@ -1,6 +1,6 @@
1
1
  class Forger::Create
2
2
  class Info
3
- include Forger::AwsService
3
+ include Forger::AwsServices
4
4
 
5
5
  attr_reader :params
6
6
  def initialize(options, params)
@@ -16,8 +16,6 @@ class Forger::Create
16
16
  end
17
17
 
18
18
  def spot(instance_id)
19
- puts "Max monthly price: $#{monthly_spot_price}/mo" if monthly_spot_price
20
-
21
19
  retries = 0
22
20
  begin
23
21
  resp = ec2.describe_instances(instance_ids: [instance_id])
@@ -33,7 +31,15 @@ class Forger::Create
33
31
  end
34
32
  end
35
33
 
36
- spot_id = resp.reservations.first.instances.first.spot_instance_request_id
34
+ reservation = resp.reservations.first
35
+ # Super edge case when reserverations not immediately found yet
36
+ until reservation
37
+ reservation = resp.reservations.first
38
+ seconds = 0.5
39
+ puts "Reserveration not found. Sleeping for #{seconds} and will try again."
40
+ sleep seconds
41
+ end
42
+ spot_id = reservation.instances.first.spot_instance_request_id
37
43
  return unless spot_id
38
44
 
39
45
  puts "Spot instance request id: #{spot_id}"
@@ -1,6 +1,6 @@
1
1
  class Forger::Create
2
2
  class Waiter < Forger::Base
3
- include Forger::AwsService
3
+ include Forger::AwsServices
4
4
 
5
5
  def wait
6
6
  @instance_id = @options[:instance_id]
@@ -1,6 +1,6 @@
1
1
  module Forger
2
2
  class Destroy < Base
3
- include AwsService
3
+ include AwsServices
4
4
 
5
5
  def run(instance_id)
6
6
  puts "Destroying #{instance_id}"
@@ -2,16 +2,4 @@ Examples:
2
2
 
3
3
  $ forger upload
4
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
- # Format 1: Simple String
10
- s3_folder: my-bucket/folder # enables auto sync to s3
11
-
12
- # Format 2: Hash
13
- # s3_folder:
14
- # default: mybucket/path/to/folder
15
- # dev_profile1: mybucket/path/to/folder
16
- # dev_profile1: another-bucket/storage/path
17
- ```
5
+ Compiles the `app/scripts` and `app/user_data` files to the tmp folder. Then uploads the files to the forger managed s3 bucket.
@@ -2,7 +2,7 @@
2
2
  # If no @vpc_id is provided to the initializer then the default vpc is used.
3
3
  module Forger
4
4
  class Network
5
- include Forger::AwsService
5
+ include Forger::AwsServices
6
6
  extend Memoist
7
7
 
8
8
  def initialize(vpc_id)
@@ -20,7 +20,7 @@ module Forger
20
20
  default_vpc.vpc_id
21
21
  else
22
22
  puts "A default vpc was not found in this AWS account and region.".color(:red)
23
- puts "Because there is no default vpc, please specify the --vpc-id option. More info: http://ufoships.com/reference/ufo-init/"
23
+ puts "Because there is no default vpc, please specify the --vpc-id option."
24
24
  exit 1
25
25
  end
26
26
  end
@@ -12,17 +12,16 @@ module Forger
12
12
  [:git, type: :boolean, default: true, desc: "Git initialize the project"],
13
13
  [:iam, desc: "iam_instance_profile to use in the profiles/default.yml"],
14
14
  [:key_name, desc: "key name to use with launched instance in profiles/default.yml"],
15
- [:s3_folder, desc: "s3_folder setting for config/settings.yml."],
16
- [:security_group, desc: "Security group to use. For config/development.yml network settings."],
17
- [:subnet, desc: "Subnet to use. For config/development.yml network settings."],
18
- [:vpc_id, desc: "Vpc id. For config/development.yml network settings. Will use default sg and subnet"],
15
+ [:security_group, desc: "Security group to use. For config/variables/development.rb network settings."],
16
+ [:subnet, desc: "Subnet to use. For config/variables/development.rb network settings."],
17
+ [:vpc_id, desc: "Vpc id. For config/variables/development.rb network settings. Will use default sg and subnet"],
19
18
  ]
20
19
  end
21
20
 
22
21
  cli_options.each do |args|
23
- class_option *args
22
+ class_option(*args)
24
23
  end
25
-
24
+
26
25
  def configure_network_settings
27
26
  return if ENV['TEST']
28
27
 
@@ -23,10 +23,23 @@ module Forger
23
23
  def load_profile(file)
24
24
  return {} unless File.exist?(file)
25
25
 
26
+ base_file, base_data = profile_file(:base), {}
27
+ if File.exist?(base_file)
28
+ puts "Detected profiles/base.yml"
29
+ base_data = yaml_load(base_file)
30
+ end
31
+
26
32
  puts "Using profile: #{file}".color(:green)
33
+ data = yaml_load(file)
34
+ data = base_data.merge(data)
35
+ data.has_key?("run_instances") ? data["run_instances"] : data
36
+ end
37
+
38
+ def yaml_load(file)
27
39
  text = RenderMePretty.result(file, context: context)
28
40
  begin
29
- data = YAML.load(text)
41
+ data = YAML.load(text) # data
42
+ data ? data : {} # in case the file is empty
30
43
  rescue Psych::SyntaxError => e
31
44
  tmp_file = file.sub("profiles", Forger.build_root)
32
45
  FileUtils.mkdir_p(File.dirname(tmp_file))
@@ -36,10 +49,9 @@ module Forger
36
49
  puts "ERROR: #{e.message}"
37
50
  exit 1
38
51
  end
39
- data ? data : {} # in case the file is empty
40
- data.has_key?("run_instances") ? data["run_instances"] : data
41
52
  end
42
53
 
54
+
43
55
  # Determines a valid profile_name. Falls back to default
44
56
  def profile_name
45
57
  # allow user to specify the path also
@@ -0,0 +1,23 @@
1
+ module Forger
2
+ class S3 < Command
3
+ desc "deploy", "deploys forger managed s3 bucket"
4
+ long_desc Help.text("s3/deploy")
5
+ def deploy
6
+ Bucket.new(options).deploy
7
+ end
8
+
9
+ desc "show", "shows forger managed s3 bucket"
10
+ long_desc Help.text("s3/show")
11
+ option :sure, type: :boolean, desc: "Bypass are you sure prompt"
12
+ def show
13
+ Bucket.new(options).show
14
+ end
15
+
16
+ desc "delete", "deletes forger managed s3 bucket"
17
+ long_desc Help.text("s3/delete")
18
+ option :sure, type: :boolean, desc: "Bypass are you sure prompt"
19
+ def delete
20
+ Bucket.new(options).delete
21
+ end
22
+ end
23
+ end