forger 1.6.0 → 2.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile +0 -2
  4. data/Gemfile.lock +30 -38
  5. data/README.md +49 -53
  6. data/docs/example/app/{user-data → user_data}/bootstrap.sh +0 -0
  7. data/forger.gemspec +1 -1
  8. data/lib/forger.rb +16 -12
  9. data/lib/forger/cli.rb +10 -4
  10. data/lib/forger/core.rb +8 -0
  11. data/lib/forger/create.rb +3 -1
  12. data/lib/forger/create/error_messages.rb +2 -2
  13. data/lib/forger/create/info.rb +18 -3
  14. data/lib/forger/create/waiter.rb +20 -4
  15. data/lib/forger/help/compile.md +1 -1
  16. data/lib/forger/help/create.md +6 -0
  17. data/lib/forger/help/new.md +33 -0
  18. data/lib/forger/help/upload.md +1 -1
  19. data/lib/forger/hook.rb +1 -1
  20. data/lib/forger/network.rb +48 -0
  21. data/lib/forger/new.rb +75 -0
  22. data/lib/forger/script.rb +1 -1
  23. data/lib/forger/script/compress.rb +1 -1
  24. data/lib/forger/script/templates/extract_forger_scripts.sh +20 -0
  25. data/lib/forger/script/upload.rb +15 -1
  26. data/lib/forger/scripts/auto_terminate/functions.sh +2 -1
  27. data/lib/forger/scripts/auto_terminate/functions/{amazonlinux2.sh → amzn.sh} +0 -0
  28. data/lib/forger/scripts/auto_terminate/functions/amzn2.sh +10 -0
  29. data/lib/forger/scripts/cloudwatch/install/{amazonlinux2.sh → amzn.sh} +0 -0
  30. data/lib/forger/scripts/cloudwatch/install/amzn2.sh +4 -0
  31. data/lib/forger/scripts/cloudwatch/service/amzn.sh +16 -0
  32. data/lib/forger/scripts/cloudwatch/service/{amazonlinux2.sh → amzn2.sh} +0 -0
  33. data/lib/forger/scripts/shared/functions.sh +1 -1
  34. data/lib/forger/sequence.rb +25 -0
  35. data/lib/forger/template/helper/core_helper.rb +21 -12
  36. data/lib/forger/template/helper/script_helper.rb +1 -1
  37. data/lib/forger/version.rb +1 -1
  38. data/lib/templates/default/.env.example +1 -0
  39. data/lib/templates/default/.gitignore +2 -0
  40. data/lib/templates/default/Gemfile +3 -0
  41. data/lib/templates/default/README.md +61 -0
  42. data/lib/templates/default/app/helpers/application_helper.rb +12 -0
  43. data/lib/templates/default/app/scripts/install/common.sh +14 -0
  44. data/lib/templates/default/app/scripts/personalize/tung.sh +3 -0
  45. data/lib/templates/default/app/scripts/shared/functions.sh +22 -0
  46. data/lib/templates/default/app/user_data/bootstrap.sh.tt +12 -0
  47. data/lib/templates/default/app/user_data/layouts/default.sh.tt +17 -0
  48. data/lib/templates/default/config/development.yml.tt +6 -0
  49. data/lib/templates/default/config/settings.yml.tt +25 -0
  50. data/lib/templates/default/profiles/default.yml.tt +41 -0
  51. data/spec/fixtures/demo_project/app/{user-data → user_data}/bootstrap.sh +0 -0
  52. data/spec/lib/core_helper_spec.rb +19 -0
  53. data/spec/spec_helper.rb +0 -4
  54. metadata +34 -12
@@ -12,6 +12,14 @@ module Forger
12
12
  Setting.new.data
13
13
  end
14
14
 
15
+ # cloudwatch cli option takes higher precedence than when its set in the
16
+ # config/settings.yml file.
17
+ def cloudwatch_enabled?(options)
18
+ !options[:cloudwatch].nil? ?
19
+ options[:cloudwatch] : # options can use symbols because this the options hash from Thor
20
+ settings["cloudwatch"] # settings uses strings as keys
21
+ end
22
+
15
23
  def root
16
24
  path = ENV['FORGER_ROOT'] || '.'
17
25
  Pathname.new(path)
@@ -58,7 +58,9 @@ module Forger
58
58
  # dev_profile1: another-bucket/storage/path
59
59
  def sync_scripts_to_s3
60
60
  return unless Forger.settings["s3_folder"]
61
- Script::Upload.new(@options).run
61
+ upload = Script::Upload.new(@options)
62
+ return if upload.empty?
63
+ upload.run
62
64
  end
63
65
 
64
66
  # params are main derived from profile files
@@ -16,8 +16,8 @@ EOL
16
16
  end
17
17
 
18
18
  # Examples:
19
- # Aws::EC2::Errors::InvalidGroupNotFound => invalid_group_not_found!
20
- # Aws::EC2::Errors::InvalidParameterCombination => invalid_parameter_combination!
19
+ # Aws::EC2::Errors::InvalidGroupNotFound => invalid_group_not_found
20
+ # Aws::EC2::Errors::InvalidParameterCombination => invalid_parameter_combination
21
21
  def map_exception_to_method(exception)
22
22
  class_name = File.basename(exception.class.to_s).sub(/.*::/,'')
23
23
  class_name.underscore # method_name
@@ -16,6 +16,8 @@ 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
+
19
21
  retries = 0
20
22
  begin
21
23
  resp = ec2.describe_instances(instance_ids: [instance_id])
@@ -23,14 +25,27 @@ class Forger::Create
23
25
  retries += 1
24
26
  puts "Aws::EC2::Errors::InvalidInstanceIDNotFound error. Retry: #{retries}"
25
27
  sleep 2**retries
26
- retry if retries <= 3
28
+ if retries <= 3
29
+ retry
30
+ else
31
+ puts "Unable to find lauched spot instance"
32
+ return
33
+ end
27
34
  end
35
+
28
36
  spot_id = resp.reservations.first.instances.first.spot_instance_request_id
29
37
  return unless spot_id
30
38
 
31
39
  puts "Spot instance request id: #{spot_id}"
32
40
  end
33
41
 
42
+ def monthly_spot_price
43
+ max_price = @params[:instance_market_options][:spot_options][:max_price].to_f
44
+ monthly_price = max_price * 24 * 30
45
+ "%.2f" % monthly_price
46
+ rescue
47
+ end
48
+
34
49
  def launch_template
35
50
  launch_template = params[:launch_template]
36
51
  return unless launch_template
@@ -53,7 +68,7 @@ class Forger::Create
53
68
  end
54
69
 
55
70
  def cloudwatch(instance_id)
56
- return unless @options[:cloudwatch]
71
+ return unless Forger.cloudwatch_enabled?(@options)
57
72
 
58
73
  region = cloudwatch_log_region
59
74
  stream = "#{instance_id}/var/log/cloud-init-output.log"
@@ -68,7 +83,7 @@ class Forger::Create
68
83
  puts " #{cw_terminate_log}"
69
84
  end
70
85
 
71
- puts "Note: It takes a little time for the instance to launch and report logs."
86
+ puts "Note: It takes at least a few minutes for the instance to launch and report logs."
72
87
 
73
88
  paste_command = show_cw ? cw_init_log : url
74
89
  add_to_clipboard(paste_command)
@@ -4,7 +4,7 @@ class Forger::Create
4
4
 
5
5
  def wait
6
6
  @instance_id = @options[:instance_id]
7
- handle_wait if @options[:wait]
7
+ handle_wait if wait?
8
8
  handle_ssh if @options[:ssh]
9
9
  end
10
10
 
@@ -23,7 +23,14 @@ class Forger::Create
23
23
  i = resp.reservations.first.instances.first
24
24
  puts "Instance #{@instance_id} is ready"
25
25
  dns = i.public_dns_name ? i.public_dns_name : 'nil'
26
- puts "Instance public_dns_name: #{dns}"
26
+ puts "Instance public dns name: #{dns}"
27
+
28
+ if i.public_dns_name && !@options[:ssh]
29
+ command = build_ssh_command(i.public_dns_name)
30
+ puts "Ssh command below. Note the user might be different. You can specify --ssh-user=USER. You can also ssh automatically into the instance with the --ssh flag."
31
+ display_ssh(command)
32
+ end
33
+
27
34
  i
28
35
  end
29
36
 
@@ -35,11 +42,16 @@ class Forger::Create
35
42
  end
36
43
 
37
44
  command = build_ssh_command(instance.public_dns_name)
38
- puts "=> #{command.join(' ')}".colorize(:green)
45
+ display_ssh(command)
39
46
  retry_until_success(command)
40
47
  Kernel.exec(*command) unless @options[:noop]
41
48
  end
42
49
 
50
+ def wait?
51
+ return false if @options[:ssh]
52
+ @options[:wait]
53
+ end
54
+
43
55
  def build_ssh_command(host)
44
56
  user = @options[:ssh_user] || "ec2-user"
45
57
  [
@@ -49,13 +61,17 @@ class Forger::Create
49
61
  ].compact
50
62
  end
51
63
 
64
+ def display_ssh(command)
65
+ puts "=> #{command.join(' ')}".colorize(:green)
66
+ end
67
+
52
68
  def retry_until_success(*command)
53
69
  retries = 0
54
70
  uptime = command + ['uptime', '2>&1']
55
71
  uptime = uptime.join(' ')
56
72
  out = `#{uptime}`
57
73
  while out !~ /load average/ do
58
- puts "Can't ssh into the server yet. Retrying until success." if retries == 0
74
+ puts "Can't ssh into the server yet. Retrying until success. (Timeout 10m)" if retries == 0
59
75
  print '.'
60
76
  retries += 1
61
77
  if retries > 600 # Timeout after 10 minutes
@@ -2,4 +2,4 @@ Examples:
2
2
 
3
3
  $ forger compile
4
4
 
5
- Compiles app/scripts and app/user-data files to the tmp folder. Useful for inspection.
5
+ Compiles app/scripts and app/user_data files to the tmp folder. Useful for inspection.
@@ -15,3 +15,9 @@ You can also tell forger to ssh into the instance immediately after it's ready w
15
15
  forger create my-instance --ssh # default is to login as ec2-user
16
16
  forger create my-instance --ssh --ssh-user ubuntu
17
17
  SSH_OPTIONS "-A" forger create my-instance --ssh --ssh-user ubuntu
18
+
19
+ ## CloudWatch support
20
+
21
+ There is experimental support for CloudWatch logs. When using the `--cloudwatch` flag, code is added to the very beginning of the user-data script so that logs of the instance are sent to cloudwatch. Example:
22
+
23
+ forger create my-instance --cloudwatch
@@ -0,0 +1,33 @@
1
+ ## Examples
2
+
3
+ forger new ec2 # project name is ec2 here
4
+ cd ec2
5
+ forger create box --noop # dry-run
6
+ forger create box # live-run
7
+
8
+ Another project name:
9
+
10
+ forger new projectname
11
+
12
+ ## S3 Folder Option
13
+
14
+ forger new --s3 folder my-s3-bucket/my-folder
15
+
16
+ ## VPC Option
17
+
18
+ forger new --vpc-id vpc-123
19
+
20
+ When the vpc-id option is not provided, forger uses the default vpc.
21
+
22
+ You can also set the security group and subnet id values explicitly instead:
23
+
24
+ forger new --subnet subnet-123
25
+ forger new --security-group sg-123
26
+
27
+ ## Iam Instance Profile
28
+
29
+ forger new --iam MyIamProfile
30
+
31
+ ## Useful Combo Starting Options
32
+
33
+ forger new ec2 --security-group sg-123 --iam MyIamProfile --key-name default --s3-folder my-s3-bucket/my-folder
@@ -2,7 +2,7 @@ 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:
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
6
 
7
7
  ```yaml
8
8
  development:
@@ -21,7 +21,7 @@ module Forger
21
21
  end
22
22
 
23
23
  def sh(command)
24
- puts "=> #{command}".colorize(:cyan)
24
+ puts "=> #{command}".colorize(:green)
25
25
  success = system(command)
26
26
  abort("Command failed") unless success
27
27
  end
@@ -0,0 +1,48 @@
1
+ # Provides access to default network settings for a vpc: subnets and security_group
2
+ # If no @vpc_id is provided to the initializer then the default vpc is used.
3
+ module Forger
4
+ class Network
5
+ include Forger::AwsService
6
+ extend Memoist
7
+
8
+ def initialize(vpc_id)
9
+ @vpc_id = vpc_id
10
+ end
11
+
12
+ def vpc_id
13
+ return @vpc_id if @vpc_id
14
+
15
+ resp = ec2.describe_vpcs(filters: [
16
+ {name: "isDefault", values: ["true"]}
17
+ ])
18
+ default_vpc = resp.vpcs.first
19
+ if default_vpc
20
+ default_vpc.vpc_id
21
+ else
22
+ puts "A default vpc was not found in this AWS account and region.".colorize(:red)
23
+ puts "Because there is no default vpc, please specify the --vpc-id option. More info: http://ufoships.com/reference/ufo-init/"
24
+ exit 1
25
+ end
26
+ end
27
+ memoize :vpc_id
28
+
29
+ # all subnets
30
+ def subnet_ids
31
+ resp = ec2.describe_subnets(filters: [
32
+ {name: "vpc-id", values: [vpc_id]}
33
+ ])
34
+ resp.subnets.map(&:subnet_id).sort
35
+ end
36
+ memoize :subnet_ids
37
+
38
+ # default security group
39
+ def security_group_id
40
+ resp = ec2.describe_security_groups(filters: [
41
+ {name: "vpc-id", values: [vpc_id]},
42
+ {name: "group-name", values: ["default"]}
43
+ ])
44
+ resp.security_groups.first.group_id
45
+ end
46
+ memoize :security_group_id
47
+ end
48
+ end
@@ -0,0 +1,75 @@
1
+ module Forger
2
+ class New < Sequence
3
+ argument :project_name
4
+
5
+ # Ugly, but when the class_option is only defined in the Thor::Group class
6
+ # it doesnt show up with cli-template new help :(
7
+ # If anyone knows how to fix this let me know.
8
+ # Also options from the cli can be pass through to here
9
+ def self.cli_options
10
+ [
11
+ [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files."],
12
+ [:git, type: :boolean, default: true, desc: "Git initialize the project"],
13
+ [:iam, desc: "iam_instance_profile to use in the profiles/default.yml"],
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"],
19
+ ]
20
+ end
21
+
22
+ cli_options.each do |args|
23
+ class_option *args
24
+ end
25
+
26
+ def configure_network_settings
27
+ return if ENV['TEST']
28
+
29
+ network = Network.new(@options[:vpc_id]) # used for default settings
30
+ @subnet = @options[:subnet] || network.subnet_ids.first
31
+ @security_group = @options[:security_group] || network.security_group_id
32
+ end
33
+
34
+ def create_project
35
+ copy_project
36
+ destination_root = "#{Dir.pwd}/#{project_name}"
37
+ self.destination_root = destination_root
38
+ FileUtils.cd("#{Dir.pwd}/#{project_name}")
39
+ end
40
+
41
+ def make_executable
42
+ chmod("exe", 0755 & ~File.umask, verbose: false) if File.exist?("exe")
43
+ end
44
+
45
+ def bundle_install
46
+ Bundler.with_clean_env do
47
+ system("BUNDLE_IGNORE_CONFIG=1 bundle install")
48
+ end
49
+ end
50
+
51
+ def git_init
52
+ return if !options[:git]
53
+ return unless git_installed?
54
+ return if File.exist?(".git") # this is a clone repo
55
+
56
+ run("git init")
57
+ run("git add .")
58
+ run("git commit -m 'first commit'")
59
+ end
60
+
61
+ def user_message
62
+ puts <<-EOL
63
+ #{"="*64}
64
+ Congrats 🎉 You have successfully generated a starter forger project.
65
+
66
+ Test the CLI:
67
+
68
+ cd #{project_name}
69
+ forger create box --noop # dry-run to see the tmp/user-data.txt script
70
+ forger create box # live-run
71
+ forger create box --ssh
72
+ EOL
73
+ end
74
+ end
75
+ end
@@ -41,7 +41,7 @@ module Forger
41
41
  private
42
42
  def load_template(name)
43
43
  template = IO.read(File.expand_path("script/templates/#{name}", File.dirname(__FILE__)))
44
- text = ERB.new(template, nil, "-").result(binding)
44
+ ERB.new(template, nil, "-").result(binding) # text
45
45
  end
46
46
  end
47
47
  end
@@ -12,7 +12,7 @@ class Forger::Script
12
12
 
13
13
  def create_tarball
14
14
  # https://apple.stackexchange.com/questions/14980/why-are-dot-underscore-files-created-and-how-can-i-avoid-them
15
- sh "cd #{BUILD_ROOT}/app && dot_clean ." if system("type dot_clean > /dev/null")
15
+ sh "cd #{BUILD_ROOT}/app && dot_clean ." if system("type dot_clean > /dev/null 2>&1")
16
16
 
17
17
  # https://serverfault.com/questions/110208/different-md5sums-for-same-tar-contents
18
18
  # Using tar czf directly results in a new m5sum each time because the gzip
@@ -36,6 +36,26 @@ function extract_forger_scripts() {
36
36
  folder="${folder#v}" # remove leading v character
37
37
  folder="forger-$folder" # IE: forger-1.0.0
38
38
 
39
+ # install wget if not installed
40
+ if ! type wget > /dev/null 2>&1 ; then
41
+ if type yum > /dev/null 2>&1 ; then
42
+ yum install -y wget
43
+ elif type apt-get > /dev/null 2>&1 ; then
44
+ apt-get update
45
+ apt-get install -y wget
46
+ fi
47
+ fi
48
+
49
+ # install tar if not installed
50
+ if ! type tar > /dev/null 2>&1 ; then
51
+ if type yum > /dev/null 2>&1 ; then
52
+ yum install -y tar
53
+ elif type apt-get > /dev/null 2>&1 ; then
54
+ apt-get update
55
+ apt-get install -y tar
56
+ fi
57
+ fi
58
+
39
59
  wget "$url"
40
60
  tar zxf "$filename"
41
61
 
@@ -38,11 +38,25 @@ class Forger::Script
38
38
  obj.upload_file(tarball_path)
39
39
  rescue Aws::S3::Errors::PermanentRedirect => e
40
40
  puts "ERROR: #{e.class} #{e.message}".colorize(:red)
41
- puts "The bucket you are trying to upload to is in a different region than the region the instance is being launched in."
41
+ puts "The bucket you are trying to upload scripts to is in a different region than the region the instance is being launched in."
42
42
  puts "You must configured FORGER_S3_ENDPOINT env variable to prevent this error. Example:"
43
43
  puts " FORGER_S3_ENDPOINT=https://s3.us-west-2.amazonaws.com"
44
44
  puts "Check your ~/.aws/config for the region being used for the ec2 instance."
45
45
  exit 1
46
+ rescue Aws::S3::Errors::AccessDenied, Aws::S3::Errors::AllAccessDisabled
47
+ e = $!
48
+ puts "ERROR: #{e.class} #{e.message}".colorize(:red)
49
+ puts "You do not have permission to upload scripts to this bucket: #{bucket_name}. Are you sure the right bucket is configured?"
50
+ if ENV['AWS_PROFILE']
51
+ puts "Also maybe check your AWS_PROFILE env. Current AWS_PROFILE=#{ENV['AWS_PROFILE']}"
52
+ end
53
+ exit 1
54
+ end
55
+
56
+ def empty?
57
+ Dir.glob("#{Forger.root}/app/scripts/**/*").select do |path|
58
+ File.file?(path)
59
+ end.empty?
46
60
  end
47
61
 
48
62
  def tarball_path
@@ -19,7 +19,8 @@ function terminate_instance() {
19
19
  INSTANCE_ID=$(wget -q -O - http://169.254.169.254/latest/meta-data/instance-id)
20
20
  SPOT_INSTANCE_REQUEST_ID=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" | jq -r '.Reservations[].Instances[].SpotInstanceRequestId')
21
21
 
22
- if [ -n "$SPOT_INSTANCE_REQUEST_ID" ]; then
22
+ # jq returns null
23
+ if [ "$SPOT_INSTANCE_REQUEST_ID" != "null" ]; then
23
24
  cancel_spot_request
24
25
  fi
25
26
  aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"
@@ -0,0 +1,10 @@
1
+ #!/bin/bash -eux
2
+ function schedule_termination() {
3
+ chmod +x /etc/rc.d/rc.local
4
+ echo "/opt/forger/auto_terminate.sh after_ami >> /var/log/auto-terminate.log 2>&1" >> /etc/rc.d/rc.local
5
+ }
6
+
7
+ function unschedule_termination() {
8
+ grep -v auto_terminate.sh /etc/rc.d/rc.local > /etc/rc.d/rc.local.tmp
9
+ mv /etc/rc.d/rc.local.tmp /etc/rc.d/rc.local
10
+ }