forger 1.6.0 → 2.0.0

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