forger 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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,161 @@
1
+ =begin
2
+ Code Explanation:
3
+
4
+ There are 3 types of things to auto-complete:
5
+
6
+ 1. command: the command itself
7
+ 2. parameters: command parameters.
8
+ 3. options: command options
9
+
10
+ Here's an example:
11
+
12
+ mycli hello name --from me
13
+
14
+ * command: hello
15
+ * parameters: name
16
+ * option: --from
17
+
18
+ When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
19
+
20
+ ## Arity
21
+
22
+ For example, say you had a method for a CLI command with the following form:
23
+
24
+ ufo scale service count --cluster development
25
+
26
+ It's equivalent ruby method:
27
+
28
+ scale(service, count) = has an arity of 2
29
+
30
+ So typing:
31
+
32
+ ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
33
+
34
+ So the completion should only show options, something like this:
35
+
36
+ --noop --verbose --cluster
37
+
38
+ ## Splat Arguments
39
+
40
+ When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
41
+
42
+ ship(service) = 1
43
+ scale(service, count) = 2
44
+ ships(*services) = -1
45
+ foo(example, *rest) = -2
46
+
47
+ Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
48
+
49
+ Here are some test cases, hit TAB after typing the command:
50
+
51
+ forger completion
52
+ forger completion hello
53
+ forger completion hello name
54
+ forger completion hello name --
55
+ forger completion hello name --noop
56
+
57
+ forger completion
58
+ forger completion sub:goodbye
59
+ forger completion sub:goodbye name
60
+
61
+ ## Subcommands and Thor::Group Registered Commands
62
+
63
+ Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
64
+
65
+ * regular command: ufo ship
66
+ * subcommand: ufo docker
67
+ * Thor::Group command: ufo init
68
+
69
+ Auto-completion accounts for each of these type of commands.
70
+ =end
71
+ module Forger
72
+ class Completer
73
+ autoload :Script, 'forger/completer/script'
74
+
75
+ def initialize(command_class, *params)
76
+ @params = params
77
+ @current_command = @params[0]
78
+ @command_class = command_class # CLI initiall
79
+ end
80
+
81
+ def run
82
+ if subcommand?(@current_command)
83
+ subcommand_class = @command_class.subcommand_classes[@current_command]
84
+ @params.shift # destructive
85
+ Completer.new(subcommand_class, *@params).run # recursively use subcommand
86
+ return
87
+ end
88
+
89
+ # full command has been found!
90
+ unless found?(@current_command)
91
+ puts all_commands
92
+ return
93
+ end
94
+
95
+ # will only get to here if command aws found (above)
96
+ arity = @command_class.instance_method(@current_command).arity.abs
97
+ if @params.size > arity or thor_group_command?
98
+ puts options_completion
99
+ else
100
+ puts params_completion
101
+ end
102
+ end
103
+
104
+ def subcommand?(command)
105
+ @command_class.subcommands.include?(command)
106
+ end
107
+
108
+ # hacky way to detect that command is a registered Thor::Group command
109
+ def thor_group_command?
110
+ command_params(raw=true) == [[:rest, :args]]
111
+ end
112
+
113
+ def found?(command)
114
+ public_methods = @command_class.public_instance_methods(false)
115
+ command && public_methods.include?(command.to_sym)
116
+ end
117
+
118
+ # all top-level commands
119
+ def all_commands
120
+ commands = @command_class.all_commands.reject do |k,v|
121
+ v.is_a?(Thor::HiddenCommand)
122
+ end
123
+ commands.keys
124
+ end
125
+
126
+ def command_params(raw=false)
127
+ params = @command_class.instance_method(@current_command).parameters
128
+ # Example:
129
+ # >> Sub.instance_method(:goodbye).parameters
130
+ # => [[:req, :name]]
131
+ # >>
132
+ raw ? params : params.map!(&:last)
133
+ end
134
+
135
+ def params_completion
136
+ offset = @params.size - 1
137
+ offset_params = command_params[offset..-1]
138
+ command_params[offset..-1].first
139
+ end
140
+
141
+ def options_completion
142
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
143
+
144
+ method_options = @command_class.all_commands[@current_command].options.keys
145
+ class_options = @command_class.class_options.keys
146
+
147
+ all_options = method_options + class_options + ['help']
148
+
149
+ all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
150
+ filtered_options = all_options - used
151
+ filtered_options.uniq
152
+ end
153
+
154
+ # Useful for debugging. Using puts messes up completion.
155
+ def log(msg)
156
+ File.open("/tmp/complete.log", "a") do |file|
157
+ file.puts(msg)
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,6 @@
1
+ class Forger::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ _forger() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+ local words=("${COMP_WORDS[@]}")
5
+ unset words[0]
6
+ local completion=$(forger completion ${words[@]})
7
+ COMPREPLY=( $(compgen -W "$completion" -- "$word") )
8
+ }
9
+
10
+ complete -F _forger forger
@@ -0,0 +1,20 @@
1
+ require 'yaml'
2
+
3
+ module Forger
4
+ class Config < Base
5
+ def initialize(options={})
6
+ super
7
+ @path = options[:path] || "#{Forger.root}/config/#{Forger.env}.yml"
8
+ end
9
+
10
+ @@data = nil
11
+ def data
12
+ return @@data if @@data
13
+ @@data = YAML.load_file(@path)
14
+ rescue Errno::ENOENT => e
15
+ puts e.message
16
+ puts "The #{@path} does not exist. Please double check that it exists."
17
+ exit
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Forger
5
+ module Core
6
+ @@config = nil
7
+ def config
8
+ @@config ||= Config.new.data
9
+ end
10
+
11
+ def settings
12
+ Setting.new.data
13
+ end
14
+
15
+ def root
16
+ path = ENV['AWS_EC2_ROOT'] || '.'
17
+ Pathname.new(path)
18
+ end
19
+
20
+ def validate_in_project!
21
+ unless File.exist?("#{root}/profiles")
22
+ puts "Could not find a profiles folder in the current directory. It does not look like you are running this command within a forger project. Please confirm that you are in a forger project and try again.".colorize(:red)
23
+ exit
24
+ end
25
+ end
26
+
27
+ @@env = nil
28
+ def env
29
+ return @@env if @@env
30
+ env = env_from_profile(ENV['AWS_PROFILE']) || 'development'
31
+ env = ENV['AWS_EC2_ENV'] if ENV['AWS_EC2_ENV'] # highest precedence
32
+ @@env = env
33
+ end
34
+
35
+ private
36
+ # Do not use the Setting class to load the profile because it can cause an
37
+ # infinite loop then if we decide to use Forger.env from within settings class.
38
+ def env_from_profile(aws_profile)
39
+ settings_path = "#{Forger.root}/config/settings.yml"
40
+ return unless File.exist?(settings_path)
41
+
42
+ data = YAML.load_file(settings_path)
43
+ env = data.find do |_env, setting|
44
+ setting ||= {}
45
+ profiles = setting['aws_profiles']
46
+ profiles && profiles.include?(aws_profile)
47
+ end
48
+ env.first if env
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,155 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Forger
5
+ class Create < Base
6
+ autoload :Params, "forger/create/params"
7
+ autoload :ErrorMessages, "forger/create/error_messages"
8
+
9
+ include AwsService
10
+ include ErrorMessages
11
+
12
+ def run
13
+ Profile.new(@options).check!
14
+
15
+ Hook.run(:before_run_instances, @options)
16
+ sync_scripts_to_s3
17
+
18
+ puts "Creating EC2 instance #{@name.colorize(:green)}"
19
+ display_ec2_info
20
+ if @options[:noop]
21
+ puts "NOOP mode enabled. EC2 instance not created."
22
+ return
23
+ end
24
+ resp = run_instances(params)
25
+ instance_id = resp.instances.first.instance_id
26
+ display_spot_info(instance_id)
27
+ puts "EC2 instance #{@name} created: #{instance_id} 🎉"
28
+ puts "Visit https://console.aws.amazon.com/ec2/home to check on the status"
29
+ display_cloudwatch_info(instance_id)
30
+ end
31
+
32
+ def run_instances(params)
33
+ ec2.run_instances(params)
34
+ rescue Aws::EC2::Errors::ServiceError => e
35
+ handle_ec2_service_error!(e)
36
+ end
37
+
38
+ # Configured by config/settings.yml.
39
+ # Example: config/settings.yml:
40
+ #
41
+ # development:
42
+ # s3_folder: my-bucket/folder
43
+ def sync_scripts_to_s3
44
+ if Forger.settings["s3_folder"]
45
+ Script::Upload.new(@options).run
46
+ end
47
+ end
48
+
49
+ # params are main derived from profile files
50
+ def params
51
+ @params ||= Params.new(@options).generate
52
+ end
53
+
54
+ def display_spot_info(instance_id)
55
+ resp = ec2.describe_instances(instance_ids: [instance_id])
56
+ spot_id = resp.reservations.first.instances.first.spot_instance_request_id
57
+ return unless spot_id
58
+
59
+ puts "Spot instance request id: #{spot_id}"
60
+ end
61
+
62
+ def display_ec2_info
63
+ puts "Using the following parameters:"
64
+ pretty_display(params)
65
+
66
+ display_launch_template
67
+ end
68
+
69
+ def display_launch_template
70
+ launch_template = params[:launch_template]
71
+ return unless launch_template
72
+
73
+ resp = ec2.describe_launch_template_versions(
74
+ launch_template_id: launch_template[:launch_template_id],
75
+ launch_template_name: launch_template[:launch_template_name],
76
+ )
77
+ versions = resp.launch_template_versions
78
+ launch_template_data = {} # combined launch_template_data
79
+ versions.sort_by { |v| v[:version_number] }.each do |v|
80
+ launch_template_data.merge!(v[:launch_template_data])
81
+ end
82
+ puts "launch template data (versions combined):"
83
+ pretty_display(launch_template_data)
84
+ rescue Aws::EC2::Errors::InvalidLaunchTemplateNameNotFoundException => e
85
+ puts "ERROR: The specified launched template #{launch_template.inspect} was not found."
86
+ puts "Please double check that it exists."
87
+ exit
88
+ end
89
+
90
+ def display_cloudwatch_info(instance_id)
91
+ return unless @options[:cloudwatch]
92
+
93
+ region = get_region
94
+ stream = "#{instance_id}/var/log/cloud-init-output.log"
95
+ url = "https://#{region}.console.aws.amazon.com/cloudwatch/home?region=#{region}#logEventViewer:group=ec2;stream=#{stream}"
96
+ cw_init_log = "cw tail -f ec2 #{stream}"
97
+ puts "To view instance's cloudwatch logs visit:"
98
+ puts " #{url}"
99
+
100
+ puts " #{cw_init_log}" if ENV['AWS_EC2_CW']
101
+ if ENV['AWS_EC2_CW'] && @options[:auto_terminate]
102
+ cw_terminate_log = "cw tail -f ec2 #{instance_id}/var/log/auto-terminate.log"
103
+ puts " #{cw_terminate_log}"
104
+ end
105
+
106
+ puts "Note: It takes a little time for the instance to launch and report logs."
107
+
108
+ paste_command = ENV['AWS_EC2_CW'] ? cw_init_log : url
109
+ add_to_clipboard(paste_command)
110
+ end
111
+
112
+ def add_to_clipboard(text)
113
+ return unless RUBY_PLATFORM =~ /darwin/
114
+ return unless system("type pbcopy > /dev/null")
115
+
116
+ system(%[echo "#{text}" | pbcopy])
117
+ puts "Pro tip: The CloudWatch Console Link has been added to your copy-and-paste clipboard."
118
+ end
119
+
120
+ def get_region
121
+ # Highest precedence: AWS_EC2_REGION env variable. Only really used here.
122
+ if ENV['AWS_EC2_REGION']
123
+ return ENV['AWS_EC2_REGION']
124
+ end
125
+
126
+ # Pretty high in precedence: AWS_PROFILE and ~/.aws/config and
127
+ aws_found = system("type aws > /dev/null")
128
+ if aws_found
129
+ region = `aws configure get region`.strip
130
+ return region
131
+ end
132
+
133
+ # Assumes instance same region as the calling ec2 instance.
134
+ # It is possible for curl not to be installed.
135
+ curl_found = system("type curl > /dev/null")
136
+ if curl_found
137
+ region = `curl --connect-timeout 3 -s 169.254.169.254/latest/meta-data/placement/availability-zone | sed s'/.$//'`
138
+ return region unless region == ''
139
+ end
140
+
141
+ return 'us-east-1' # fallback default
142
+ end
143
+
144
+ def pretty_display(data)
145
+ data = data.deep_stringify_keys
146
+
147
+ if data["user_data"]
148
+ message = "base64-encoded: cat tmp/user-data.txt to view"
149
+ data["user_data"] = message
150
+ end
151
+
152
+ puts YAML.dump(data)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,58 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ class Forger::Create
4
+ module ErrorMessages
5
+ def handle_ec2_service_error!(exception)
6
+ meth = map_exception_to_method(exception)
7
+ if respond_to?(meth)
8
+ message = send(meth) # custom specific error message
9
+ message = print_error_message(exception, message)
10
+ else
11
+ # generic error message
12
+ print_error_message(exception, <<-EOL)
13
+ There was an error with the parameters used for the run_instance method.
14
+ EOL
15
+ end
16
+ end
17
+
18
+ # Examples:
19
+ # Aws::EC2::Errors::InvalidGroupNotFound => invalid_group_not_found!
20
+ # Aws::EC2::Errors::InvalidParameterCombination => invalid_parameter_combination!
21
+ def map_exception_to_method(exception)
22
+ class_name = File.basename(exception.class.to_s).sub(/.*::/,'')
23
+ class_name.underscore # method_name
24
+ end
25
+
26
+ def print_error_message(exception, message)
27
+ puts "ERROR: Unable to launch the instance.".colorize(:red)
28
+ puts message
29
+ puts exception.message
30
+ puts "For the full internal backtrace re-run the command with DEBUG=1"
31
+ puts exception.backtrace if ENV['DEBUG']
32
+ exit 1
33
+ end
34
+
35
+ #######################################################
36
+ # specific messages with a little more info for more common error cases below:
37
+ def invalid_group_not_found
38
+ <<-EOL
39
+ The security group passed in does not exit.
40
+ Please double check that security group exists in the VPC.
41
+ EOL
42
+ end
43
+
44
+ def invalid_parameter_combination
45
+ <<-EOL
46
+ The parameters passed to the run_instances method were invalid.
47
+ Please double check that the parameters are all valid.
48
+ EOL
49
+ end
50
+
51
+ def invalid_subnet_id_not_found
52
+ <<-EOL
53
+ The provided subnets ids were were not found.
54
+ Please double check that the subnets exists.
55
+ EOL
56
+ end
57
+ end
58
+ end