slugforge 4.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 (72) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +316 -0
  3. data/bin/slugforge +9 -0
  4. data/lib/slugforge.rb +19 -0
  5. data/lib/slugforge/build.rb +4 -0
  6. data/lib/slugforge/build/build_project.rb +31 -0
  7. data/lib/slugforge/build/export_upstart.rb +85 -0
  8. data/lib/slugforge/build/package.rb +63 -0
  9. data/lib/slugforge/cli.rb +125 -0
  10. data/lib/slugforge/commands.rb +130 -0
  11. data/lib/slugforge/commands/build.rb +20 -0
  12. data/lib/slugforge/commands/config.rb +24 -0
  13. data/lib/slugforge/commands/deploy.rb +383 -0
  14. data/lib/slugforge/commands/project.rb +21 -0
  15. data/lib/slugforge/commands/tag.rb +148 -0
  16. data/lib/slugforge/commands/wrangler.rb +142 -0
  17. data/lib/slugforge/configuration.rb +125 -0
  18. data/lib/slugforge/helper.rb +186 -0
  19. data/lib/slugforge/helper/build.rb +46 -0
  20. data/lib/slugforge/helper/config.rb +37 -0
  21. data/lib/slugforge/helper/enumerable.rb +46 -0
  22. data/lib/slugforge/helper/fog.rb +90 -0
  23. data/lib/slugforge/helper/git.rb +89 -0
  24. data/lib/slugforge/helper/path.rb +76 -0
  25. data/lib/slugforge/helper/project.rb +86 -0
  26. data/lib/slugforge/models/host.rb +233 -0
  27. data/lib/slugforge/models/host/fog_host.rb +33 -0
  28. data/lib/slugforge/models/host/hostname_host.rb +9 -0
  29. data/lib/slugforge/models/host/ip_address_host.rb +9 -0
  30. data/lib/slugforge/models/host_group.rb +65 -0
  31. data/lib/slugforge/models/host_group/aws_tag_group.rb +22 -0
  32. data/lib/slugforge/models/host_group/ec2_instance_group.rb +21 -0
  33. data/lib/slugforge/models/host_group/hostname_group.rb +16 -0
  34. data/lib/slugforge/models/host_group/ip_address_group.rb +16 -0
  35. data/lib/slugforge/models/host_group/security_group_group.rb +20 -0
  36. data/lib/slugforge/models/logger.rb +36 -0
  37. data/lib/slugforge/models/tag_manager.rb +125 -0
  38. data/lib/slugforge/slugins.rb +125 -0
  39. data/lib/slugforge/version.rb +9 -0
  40. data/scripts/post-install.sh +143 -0
  41. data/scripts/unicorn-shepherd.sh +305 -0
  42. data/spec/fixtures/array.yaml +3 -0
  43. data/spec/fixtures/fog_credentials.yaml +4 -0
  44. data/spec/fixtures/invalid_syntax.yaml +1 -0
  45. data/spec/fixtures/one.yaml +3 -0
  46. data/spec/fixtures/two.yaml +3 -0
  47. data/spec/fixtures/valid.yaml +4 -0
  48. data/spec/slugforge/commands/deploy_spec.rb +72 -0
  49. data/spec/slugforge/commands_spec.rb +33 -0
  50. data/spec/slugforge/configuration_spec.rb +200 -0
  51. data/spec/slugforge/helper/fog_spec.rb +81 -0
  52. data/spec/slugforge/helper/git_spec.rb +152 -0
  53. data/spec/slugforge/models/host_group/aws_tag_group_spec.rb +54 -0
  54. data/spec/slugforge/models/host_group/ec2_instance_group_spec.rb +51 -0
  55. data/spec/slugforge/models/host_group/hostname_group_spec.rb +20 -0
  56. data/spec/slugforge/models/host_group/ip_address_group_spec.rb +54 -0
  57. data/spec/slugforge/models/host_group/security_group_group_spec.rb +52 -0
  58. data/spec/slugforge/models/tag_manager_spec.rb +75 -0
  59. data/spec/spec_helper.rb +37 -0
  60. data/spec/support/env.rb +3 -0
  61. data/spec/support/example_groups/configuration_writer.rb +24 -0
  62. data/spec/support/example_groups/helper_provider.rb +10 -0
  63. data/spec/support/factories.rb +18 -0
  64. data/spec/support/fog.rb +15 -0
  65. data/spec/support/helpers.rb +18 -0
  66. data/spec/support/mock_logger.rb +6 -0
  67. data/spec/support/ssh.rb +8 -0
  68. data/spec/support/streams.rb +13 -0
  69. data/templates/foreman/master.conf.erb +21 -0
  70. data/templates/foreman/process-master.conf.erb +2 -0
  71. data/templates/foreman/process.conf.erb +19 -0
  72. metadata +344 -0
@@ -0,0 +1,46 @@
1
+ module Slugforge
2
+ module Helper
3
+ module Build
4
+ def verify_procfile_exists!
5
+ unless File.exist?(project_path('Procfile'))
6
+ logger.say_status :warning, "Slugforge should normally be run in a project with a Procfile (#{project_path('Procfile')})", :yellow
7
+ end
8
+ end
9
+
10
+ def ruby_version_specified?
11
+ options[:ruby] and !options[:ruby].empty?
12
+ end
13
+
14
+ def has_ruby_version_file?
15
+ File.exist?(project_path('.ruby-version'))
16
+ end
17
+
18
+ def get_ruby_version_from_file
19
+ ruby_version = read_from_file
20
+ if ruby_version.nil? or ruby_version.empty?
21
+ raise error_class, "You don't have a ruby version specified in your .ruby-version file!!! Why you no set ruby version."
22
+ else
23
+ return ruby_version
24
+ end
25
+ end
26
+
27
+ def read_from_file
28
+ begin
29
+ File.read(project_path('.ruby-version')).delete("\n")
30
+ rescue Exception => e
31
+ raise error_class, "There were issues reading the .ruby-version file. Make sure it exists in the project path and it has valid content, #{e}."
32
+ end
33
+ end
34
+
35
+ def package_file_name
36
+ "#{project_name}-#{date_stamp}-#{git_sha}.slug"
37
+ end
38
+
39
+ def date_stamp
40
+ # Keep this as a class variable so the date stamp remains the same throughought the lifecycle of the app.
41
+ @@date_stamp ||= Time.now.strftime('%Y%m%d%H%M%S')
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,37 @@
1
+ module Slugforge
2
+ module Helper
3
+ module Config
4
+ def self.included(base)
5
+ base.class_option :'aws-access-key-id', :type => :string, :aliases => '-I', :group => :config,
6
+ :desc => 'The AWS Access ID to use for hosts and buckets, unless overridden'
7
+ base.class_option :'aws-secret-key', :type => :string, :aliases => '-S', :group => :config,
8
+ :desc => 'The AWS Secret Key to use for hosts and buckets, unless overridden'
9
+ base.class_option :'aws-region', :type => :string, :group => :config,
10
+ :desc => 'The AWS region to use for EC2 instances and buckets'
11
+ base.class_option :'slug-bucket', :type => :string, :group => :config,
12
+ :desc => 'The S3 bucket to store the slugs and tags in'
13
+ base.class_option :'aws-session-token', :type => :string, :group => :config,
14
+ :desc => 'The AWS Session Token to use for hosts and buckets'
15
+
16
+ base.class_option :project, :type => :string, :aliases => '-P', :group => :config,
17
+ :desc => 'The name of the project as it exists in Slugforge. See the Project Naming section in the main help.'
18
+
19
+ base.class_option :'ssh-username', :type => :string, :aliases => '-u', :group => :config,
20
+ :desc => 'The account used to log in to the host (requires sudo privileges)'
21
+
22
+ base.class_option :'disable-slugins', :type => :boolean, :group => :config,
23
+ :desc => 'Disable slugin loading'
24
+
25
+ base.class_option :verbose, :type => :boolean, :aliases => '-V', :group => :runtime,
26
+ :desc => 'Display verbose output'
27
+ base.class_option :json, :type => :boolean, :aliases => '-j', :group => :runtime,
28
+ :desc => 'Display JSON output'
29
+
30
+ # Options intended for slugforge developers
31
+ base.class_option :test, :type => :boolean, :group => :runtime, :hide => true,
32
+ :desc => 'Test mode. Behaves like --pretend but triggers notifications and side effects as if a real action was taken.'
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,46 @@
1
+ module Enumerable
2
+ def parallel_map
3
+ queue = Queue.new
4
+
5
+ self.map do |item|
6
+ Thread.new do
7
+ # NOTE: You can not do anything that is not thread safe in this block...
8
+ queue << yield(item)
9
+ end
10
+ end.each(&:join)
11
+
12
+ [].tap do |results|
13
+ results << queue.pop until queue.empty?
14
+ end
15
+ end
16
+
17
+ def parallel_map_with_index
18
+ queue = Queue.new
19
+
20
+ self.map.with_index do |item, index|
21
+ Thread.new do
22
+ # NOTE: You can not do anything that is not thread safe in this block...
23
+ queue << yield(item, index)
24
+ end
25
+ end.each(&:join)
26
+
27
+ [].tap do |results|
28
+ results << queue.pop until queue.empty?
29
+ end
30
+ end
31
+
32
+ def ordered_parallel_map
33
+ queue = Queue.new
34
+
35
+ self.map.with_index do |item, index|
36
+ Thread.new do
37
+ # NOTE: You can not do anything that is not thread safe in this block...
38
+ queue << [index, yield(item)]
39
+ end
40
+ end.each(&:join)
41
+
42
+ [].tap do |results|
43
+ results << queue.pop until queue.empty?
44
+ end.sort.map {|index, item| item }
45
+ end
46
+ end
@@ -0,0 +1,90 @@
1
+ require 'fog'
2
+
3
+ module Slugforge
4
+ module Helper
5
+ module Fog
6
+ def compute
7
+ @compute ||= ::Fog::Compute.new(aws_credentials.merge({
8
+ :region => config.aws_region,
9
+ :provider => 'AWS'
10
+ }))
11
+ end
12
+
13
+ def autoscaling
14
+ @autoscaling ||= ::Fog::AWS::AutoScaling.new(aws_credentials)
15
+ end
16
+
17
+ def s3
18
+ @s3 ||= ::Fog::Storage.new(aws_credentials.merge({
19
+ :provider => 'AWS'
20
+ }))
21
+ end
22
+
23
+ def aws_credentials
24
+ {
25
+ :aws_access_key_id => verify_aws_config(config.aws_access_key, 'access key'),
26
+ :aws_secret_access_key => verify_aws_config(config.aws_secret_key, 'secret key'),
27
+ :aws_session_token => config.aws_session_token
28
+ }.reject{ |_,v| v.nil? }
29
+ end
30
+
31
+ def aws_bucket
32
+ config.slug_bucket || raise(error_class, "You must specify a slug bucket in your configuration")
33
+ end
34
+
35
+ def expiring_url(file, expiration=nil)
36
+ expiration ||= Time.now + 60*60
37
+ file.url(expiration)
38
+ end
39
+
40
+ # Create a temporary AWS session
41
+ # @return [Hash] hash containing :access_key_id, :secret_access_key, :session_token
42
+ def aws_session(duration = 30)
43
+ @aws_session ||= begin
44
+ sts = ::Fog::AWS::STS.new(aws_credentials)
45
+
46
+ # Request a token for the user that has permissions masked to a single S3 bucket and only lasts a short time
47
+ token = sts.get_federation_token( username, bucket_policy, duration * 60 ) # session duration in minutes
48
+
49
+ {
50
+ aws_access_key_id: token.body['AccessKeyId'],
51
+ aws_secret_access_key: token.body['SecretAccessKey'],
52
+ aws_session_token: token.body['SessionToken'],
53
+ aws_region: config.aws_region
54
+ }
55
+ end
56
+ end
57
+
58
+ private
59
+ def username
60
+ `whoami`.chomp
61
+ end
62
+
63
+ def verify_aws_config(variable, message)
64
+ raise error_class, "AWS #{message} is required to access AWS" unless variable
65
+ variable
66
+ end
67
+
68
+ def bucket_policy(bucket = aws_bucket)
69
+ {
70
+ "Version" => "2012-10-17",
71
+ "Statement" => [
72
+ {
73
+ "Action" => ["s3:*"],
74
+ "Effect" => "Allow",
75
+ "Resource" => ["arn:aws:s3:::#{bucket}/*"]
76
+ },
77
+ {
78
+ "Action" => [
79
+ "s3:ListBucket"
80
+ ],
81
+ "Effect" => "Allow",
82
+ "Resource" => [ "arn:aws:s3:::#{bucket}" ]
83
+ }
84
+ ]
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,89 @@
1
+ module Slugforge
2
+ module Helper
3
+ module Git
4
+
5
+ SHA_MAX_LENGTH = 10
6
+
7
+ def git_inside_work_tree?
8
+ return @git_inside_work_tree unless @git_inside_work_tree.nil?
9
+ @git_inside_work_tree = git_command('rev-parse --is-inside-work-tree') == 'true'
10
+ end
11
+
12
+ def git_user
13
+ @git_user ||= git_command('config github.user')
14
+ end
15
+
16
+ def git_account
17
+ return nil unless git_inside_work_tree? && !git_url.empty?
18
+ @git_account ||= git_url.match(%r|[:/]([^/]+)/[^/]+(\.git)?$|)[1]
19
+ end
20
+
21
+ def git_repository
22
+ return nil unless git_inside_work_tree? && !git_url.empty?
23
+ @git_repository ||= git_url.match(%r|/([^/]+?)(\.git)?$|)[1]
24
+ end
25
+
26
+ def git_branch
27
+ return nil unless git_inside_work_tree?
28
+ @git_branch ||= begin
29
+ symbolic_ref = git_command('symbolic-ref HEAD')
30
+ symbolic_ref.sub(%r|^refs/heads/|, '')
31
+ end
32
+ end
33
+
34
+ def git_remote
35
+ return nil unless git_inside_work_tree?
36
+ @git_remote ||= git_command("config branch.#{git_branch}.remote")
37
+ # If we are headless just assume origin so that we can still detect other values
38
+ @git_remote.empty? ? 'origin' : @git_remote
39
+ end
40
+
41
+ def git_remote_sha(opts = {})
42
+ return nil unless git_inside_work_tree?
43
+ sha_length = opts[:sha_length] || SHA_MAX_LENGTH
44
+ url = opts[:url] || git_url
45
+ branch = opts[:branch] || git_branch
46
+
47
+ @git_remote_sha = begin
48
+ if @git_remote_sha.nil? || opts[:memoize] == false
49
+ output = git_command("ls-remote #{url} #{branch}").split(" ").first
50
+ output =~ /^[0-9a-f]{40}$/i ? output : nil
51
+ else
52
+ @git_remote_sha
53
+ end
54
+ end
55
+
56
+ return @git_remote_sha.slice(0...sha_length) unless @git_remote_sha.nil?
57
+ end
58
+
59
+ def git_sha(opts = {})
60
+ raise error_class, "SHA can't be detected as this is not a git repository" unless git_inside_work_tree?
61
+ sha_length = opts[:sha_length] || SHA_MAX_LENGTH
62
+ @git_sha ||= git_command('rev-parse HEAD').chomp
63
+ @git_sha.slice(0...sha_length)
64
+ end
65
+
66
+ def git_url
67
+ return '' unless git_inside_work_tree?
68
+ @git_url ||= git_command("config remote.#{git_remote}.url")
69
+ end
70
+
71
+ def build_git_url(account, repository)
72
+ account ||= git_account
73
+ repository ||= git_repository
74
+ "git@github.com:#{account}/#{repository}.git"
75
+ end
76
+
77
+ private
78
+ def git_command(cmd)
79
+ path = options[:path] ? "cd '#{options[:path]}' &&" : ""
80
+ `#{path} git #{cmd} 2> /dev/null`.chomp
81
+ end
82
+
83
+ def git_info
84
+ Hash[methods.select { |m| m.to_s =~/^git_/ }.map { |m| [ m.to_s, send(m) ] }]
85
+ end
86
+ end
87
+ end
88
+ end
89
+
@@ -0,0 +1,76 @@
1
+ require 'bundler'
2
+
3
+ module Slugforge
4
+ module Helper
5
+ module Path
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.source_root base.templates_dir
9
+ end
10
+
11
+ def project_path(*paths)
12
+ File.join(project_root, *paths)
13
+ end
14
+
15
+ def project_root
16
+ return @locate_project unless @locate_project.nil?
17
+ if options[:path] && Dir.exist?(File.expand_path(options[:path]))
18
+ return File.expand_path(options[:path])
19
+ end
20
+
21
+ path = File.expand_path(Dir.pwd)
22
+ paths = path.split('/')
23
+ until paths.empty?
24
+ if Dir.exist?(File.join(*paths, '.git'))
25
+ @locate_project = File.join(*paths)
26
+ return @locate_project
27
+ end
28
+
29
+ paths.pop
30
+ end
31
+ raise error_class, "Invalid path. Unable to find a .git project anywhere in path #{path}. Specify a path with --path."
32
+ end
33
+
34
+ def upstart_dir
35
+ @upstart_conf_dir ||= project_path('deploy', 'upstart').tap do |dir|
36
+ FileUtils.mkdir_p(dir)
37
+ end
38
+
39
+ end
40
+
41
+ def scripts_dir(*paths)
42
+ File.join(self.class.scripts_dir, *paths)
43
+ end
44
+
45
+ def templates_dir(*paths)
46
+ File.join(self.class.templates_dir, *paths)
47
+ end
48
+
49
+ def deploy_dir(*paths)
50
+ @deploy_dir ||= File.join('/opt', 'apps', project_name)
51
+ File.join(@deploy_dir, *paths)
52
+ end
53
+
54
+ def release_dir(*paths)
55
+ deploy_dir('releases', sha)
56
+ end
57
+
58
+ def system_with_path(cmd, path=nil)
59
+ path ||= options[:path]
60
+ cwd_command = path ? "cd #{path} && " : ""
61
+ ::Bundler.with_clean_env { system("#{cwd_command}#{cmd}") }
62
+ end
63
+
64
+ module ClassMethods
65
+ def scripts_dir
66
+ @scripts_dir ||= File.expand_path('../../../scripts', File.dirname(__FILE__))
67
+ end
68
+
69
+ def templates_dir
70
+ @templates_dir ||= File.expand_path('../../../templates', File.dirname(__FILE__))
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,86 @@
1
+ module Slugforge
2
+ module Helper
3
+ module Project
4
+ def initialize(args=[], options={}, config={})
5
+ super
6
+ # Tracking the current command so we can be safe when creating projects
7
+ @current_command = config[:current_command]
8
+ end
9
+
10
+ protected
11
+ def tag_manager
12
+ @tag_manager ||= TagManager.new(:s3 => s3, :bucket => aws_bucket)
13
+ end
14
+
15
+ def bucket
16
+ @bucket ||= s3.directories.get(aws_bucket)
17
+ end
18
+
19
+ def project_name
20
+ # First one to return a value wins!
21
+ @project_name ||= config.project || git_repository
22
+ raise error_class, "Could not determine project name. This repository probably doesn't have an upstream branch yet. Please push your code, or specify `--project` when running slugforge." if @project_name.nil?
23
+ @project_name
24
+ end
25
+
26
+ def verify_project_name!(project=nil, opts={})
27
+ project ||= project_name
28
+ tm = TagManager.new(:s3 => s3, :bucket => aws_bucket)
29
+ return if tm.projects.include?(project)
30
+ raise error_class, "Project name could not be determined" unless project
31
+ end
32
+
33
+ def files
34
+ # If a block is provided, filter the files before mapping them
35
+ files = block_given? ? yield(bucket.files) : bucket.files
36
+ Hash[files.parallel_map_with_index do |file, i|
37
+ key = file.key.split('/', 2)
38
+
39
+ file.attributes.merge!({
40
+ :index => i,
41
+ :name => key.last,
42
+ :project => key.first,
43
+ :age => (Time.now.to_f - file.last_modified.to_f),
44
+ :pretty_age => format_age(file.last_modified),
45
+ :pretty_length => format_size(file.content_length)
46
+ })
47
+
48
+ [file.key, file]
49
+ end]
50
+ end
51
+
52
+ def slugs(project)
53
+ filter = Proc.new do |files|
54
+ result=[]
55
+ # Fog only #maps against the first 1000 items, so we will use #each instead
56
+ files.each { |file| result << file if file.key =~ /^#{project}\/.*\.slug$/ } # ex match: project/blag.slug
57
+ result
58
+ end
59
+ files(&filter)
60
+ end
61
+
62
+ def find_latest_slug
63
+ self.slugs(project_name).values.sort_by { |s| s.last_modified }.last
64
+ end
65
+
66
+ # finds a slug with name_part somewhere in the name. Use enough of the name to make
67
+ # it unique or this will just return the first slug it finds
68
+ def find_slug(name_part)
69
+ s = self.slugs(project_name).values.find_all { |f| f.attributes[:key].include?(name_part) }
70
+ if s.size == 0
71
+ raise error_class, "unable to find a slug from '#{name_part}'. Use 'wrangler list' command to see available slugs"
72
+ elsif s.size > 1
73
+ raise error_class, "ambiguous slug name. Found more than one slug with '#{name_part}' in their names.\n#{s.map{|sl| File.basename(sl.key)} * "\n"}\n Use 'wrangler list' command to see available slugs"
74
+ end
75
+ s[0]
76
+ end
77
+
78
+ def find_slug_name(pattern)
79
+ slugs = self.slugs(project_name).values.select { |f| f.attributes[:key] =~ pattern }
80
+ return nil if slugs.empty?
81
+ slugs.sort_by { |s| s.last_modified }.last.key
82
+ end
83
+ end
84
+ end
85
+ end
86
+