statistrano 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/changelog.md +161 -0
  3. data/doc/config/file-permissions.md +33 -0
  4. data/doc/config/log-files.md +32 -0
  5. data/doc/config/task-definitions.md +88 -0
  6. data/doc/getting-started.md +96 -0
  7. data/doc/strategies/base.md +38 -0
  8. data/doc/strategies/branches.md +82 -0
  9. data/doc/strategies/releases.md +110 -0
  10. data/doc/strategies.md +17 -0
  11. data/lib/statistrano/config/configurable.rb +53 -0
  12. data/lib/statistrano/config/rake_task_with_context_creation.rb +43 -0
  13. data/lib/statistrano/config.rb +52 -0
  14. data/lib/statistrano/deployment/log_file.rb +44 -0
  15. data/lib/statistrano/deployment/manifest.rb +88 -0
  16. data/lib/statistrano/deployment/rake_tasks.rb +74 -0
  17. data/lib/statistrano/deployment/registerable.rb +11 -0
  18. data/lib/statistrano/deployment/releaser/revisions.rb +163 -0
  19. data/lib/statistrano/deployment/releaser/single.rb +48 -0
  20. data/lib/statistrano/deployment/releaser.rb +2 -0
  21. data/lib/statistrano/deployment/strategy/base.rb +132 -0
  22. data/lib/statistrano/deployment/strategy/branches/index/template.html.erb +78 -0
  23. data/lib/statistrano/deployment/strategy/branches/index.rb +40 -0
  24. data/lib/statistrano/deployment/strategy/branches/release.rb +73 -0
  25. data/lib/statistrano/deployment/strategy/branches.rb +198 -0
  26. data/lib/statistrano/deployment/strategy/check_git.rb +43 -0
  27. data/lib/statistrano/deployment/strategy/invoke_tasks.rb +58 -0
  28. data/lib/statistrano/deployment/strategy/releases.rb +76 -0
  29. data/lib/statistrano/deployment/strategy.rb +37 -0
  30. data/lib/statistrano/deployment.rb +10 -0
  31. data/lib/statistrano/log/default_logger.rb +105 -0
  32. data/lib/statistrano/log.rb +33 -0
  33. data/lib/statistrano/remote/file.rb +79 -0
  34. data/lib/statistrano/remote.rb +111 -0
  35. data/lib/statistrano/shell.rb +17 -0
  36. data/lib/statistrano/util/file_permissions.rb +34 -0
  37. data/lib/statistrano/util.rb +27 -0
  38. data/lib/statistrano/version.rb +3 -0
  39. data/lib/statistrano.rb +55 -0
  40. data/readme.md +247 -0
  41. data/spec/integration_tests/base_integration_spec.rb +103 -0
  42. data/spec/integration_tests/branches_integration_spec.rb +189 -0
  43. data/spec/integration_tests/releases/deploy_integration_spec.rb +116 -0
  44. data/spec/integration_tests/releases/list_releases_integration_spec.rb +38 -0
  45. data/spec/integration_tests/releases/prune_releases_integration_spec.rb +86 -0
  46. data/spec/integration_tests/releases/rollback_release_integration_spec.rb +46 -0
  47. data/spec/lib/statistrano/config/configurable_spec.rb +88 -0
  48. data/spec/lib/statistrano/config/rake_task_with_context_creation_spec.rb +73 -0
  49. data/spec/lib/statistrano/config_spec.rb +34 -0
  50. data/spec/lib/statistrano/deployment/log_file_spec.rb +75 -0
  51. data/spec/lib/statistrano/deployment/manifest_spec.rb +171 -0
  52. data/spec/lib/statistrano/deployment/rake_tasks_spec.rb +107 -0
  53. data/spec/lib/statistrano/deployment/registerable_spec.rb +19 -0
  54. data/spec/lib/statistrano/deployment/releaser/revisions_spec.rb +486 -0
  55. data/spec/lib/statistrano/deployment/releaser/single_spec.rb +59 -0
  56. data/spec/lib/statistrano/deployment/strategy/base_spec.rb +158 -0
  57. data/spec/lib/statistrano/deployment/strategy/branches_spec.rb +19 -0
  58. data/spec/lib/statistrano/deployment/strategy/check_git_spec.rb +39 -0
  59. data/spec/lib/statistrano/deployment/strategy/invoke_tasks_spec.rb +66 -0
  60. data/spec/lib/statistrano/deployment/strategy/releases_spec.rb +257 -0
  61. data/spec/lib/statistrano/deployment/strategy_spec.rb +76 -0
  62. data/spec/lib/statistrano/deployment_spec.rb +4 -0
  63. data/spec/lib/statistrano/log/default_logger_spec.rb +172 -0
  64. data/spec/lib/statistrano/log_spec.rb +36 -0
  65. data/spec/lib/statistrano/remote/file_spec.rb +166 -0
  66. data/spec/lib/statistrano/remote_spec.rb +226 -0
  67. data/spec/lib/statistrano/util/file_permissions_spec.rb +25 -0
  68. data/spec/lib/statistrano/util_spec.rb +23 -0
  69. data/spec/lib/statistrano_spec.rb +52 -0
  70. data/spec/spec_helper.rb +86 -0
  71. data/spec/support/given.rb +39 -0
  72. metadata +223 -0
@@ -0,0 +1,37 @@
1
+ module Statistrano
2
+ module Deployment
3
+ module Strategy
4
+
5
+ class << self
6
+
7
+ def registered
8
+ @_registered ||= {}
9
+ end
10
+
11
+ def register deployment, name
12
+ registered[name.to_sym] = deployment
13
+ end
14
+
15
+ def find name
16
+ registered.fetch(name.to_sym) do
17
+ raise UndefinedStrategy, "no strategies are registered as :#{name}"
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ class UndefinedStrategy < StandardError
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+
30
+ # strategy utils
31
+ require_relative 'strategy/invoke_tasks'
32
+ require_relative 'strategy/check_git'
33
+
34
+ # strategies
35
+ require_relative 'strategy/base'
36
+ require_relative 'strategy/branches'
37
+ require_relative 'strategy/releases'
@@ -0,0 +1,10 @@
1
+ # deployment utils
2
+ require_relative 'deployment/manifest'
3
+ require_relative 'deployment/log_file'
4
+ require_relative 'deployment/rake_tasks'
5
+ require_relative 'deployment/registerable'
6
+
7
+
8
+ # deployment types
9
+ require_relative 'deployment/strategy'
10
+ require_relative 'deployment/releaser'
@@ -0,0 +1,105 @@
1
+ module Statistrano
2
+ class Log
3
+
4
+
5
+ # Error, Warning and Message Logging
6
+ class DefaultLogger
7
+
8
+ def info *msg
9
+ status, msg = extract_status "", *msg
10
+
11
+ case status
12
+ when :success then
13
+ color = :green
14
+ else
15
+ color = :bright
16
+ end
17
+
18
+ to_stdout status, color, *msg
19
+ end
20
+ alias_method :debug, :info
21
+
22
+ def warn *msg
23
+ status, msg = extract_status "warning", *msg
24
+ to_stdout status, :yellow, *msg
25
+ end
26
+
27
+ def error *msg
28
+ status, msg = extract_status "error", *msg
29
+ to_stderr status, :red, *msg
30
+ end
31
+ alias_method :fatal, :error
32
+
33
+ private
34
+
35
+ def extract_status default, *msg
36
+ if msg.first.is_a? Symbol
37
+ status = msg.shift
38
+ else
39
+ status = default
40
+ end
41
+ [status, msg]
42
+ end
43
+
44
+ def to_stdout status, color, *msg
45
+ $stdout.puts "#{Formatter.new(status, color, *msg).output}"
46
+ $stdout.flush
47
+ end
48
+
49
+ def to_stderr status, color, *msg
50
+ $stderr.puts "#{Formatter.new(status, color, *msg).output}"
51
+ $stderr.flush
52
+ end
53
+
54
+ class Formatter
55
+ attr_reader :width, :status, :color, :msgs
56
+
57
+ def initialize status, color, *msg
58
+ @width = 14
59
+ @status = status.to_s
60
+ @color = color
61
+ @msgs = msg
62
+ end
63
+
64
+ def output
65
+ Rainbow(anchor).bright + padding + Rainbow(status).public_send(color) + formatted_messages
66
+ end
67
+
68
+ private
69
+
70
+ def anchor
71
+ "-> "
72
+ end
73
+
74
+ def padding
75
+ num = (width - status.length)
76
+
77
+ if num < 0
78
+ @width = status.length + 1
79
+ return spaces(0)
80
+ else
81
+ return spaces num
82
+ end
83
+ end
84
+
85
+ def spaces num
86
+ Array.new(num).join(" ")
87
+ end
88
+
89
+ def formatted_messages
90
+ messages = []
91
+ msgs.each_with_index do |msg, idx|
92
+ if idx == 0
93
+ messages << " #{msg}"
94
+ else
95
+ messages << "#{spaces( anchor.length + width )} #{msg}"
96
+ end
97
+ end
98
+ messages.join("\n")
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+ require_relative 'log/default_logger'
3
+
4
+ module Statistrano
5
+
6
+ # interface should match the ruby logger
7
+ # so we will implement:
8
+ #
9
+ # => fatal
10
+ # => error
11
+ # => warn
12
+ # => info
13
+ # => debug
14
+ #
15
+ # note that DefaultLogger does accept multiline logs
16
+ # as *args so you will need a wrapper for some logging libraries
17
+
18
+ class Log
19
+ extend SingleForwardable
20
+ def_delegators :logger_instance, :fatal, :error, :warn, :info, :debug
21
+
22
+ class << self
23
+ def set_logger logger
24
+ @_logger = logger
25
+ end
26
+
27
+ def logger_instance
28
+ @_logger ||= DefaultLogger.new
29
+ end
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,79 @@
1
+ module Statistrano
2
+ class Remote
3
+
4
+ class File
5
+
6
+ attr_reader :path, :remote, :permissions
7
+
8
+ def initialize path, remote, permissions=644
9
+ @path = path
10
+ @remote = remote
11
+ @permissions = permissions
12
+ end
13
+
14
+ def content
15
+ resp = remote.run "cat #{path}"
16
+ if resp.success?
17
+ resp.stdout
18
+ else
19
+ ""
20
+ end
21
+ end
22
+
23
+ def update_content! new_content
24
+ create_remote_file unless remote_file_exists?
25
+ resp = remote.run "echo '#{new_content}' > #{path}"
26
+
27
+ if resp.success?
28
+ Log.info :success, "file at #{path} on #{remote.config.hostname} saved"
29
+ else
30
+ Log.error "problem saving the file #{path} on #{remote.config.hostname}",
31
+ resp.stderr
32
+ end
33
+ end
34
+
35
+ def append_content! new_content
36
+ create_remote_file unless remote_file_exists?
37
+ resp = remote.run "echo '#{new_content}' >> #{path}"
38
+
39
+ if resp.success?
40
+ Log.info :success, "appended content to file at #{path} on #{remote.config.hostname}"
41
+ else
42
+ Log.error "problem appending content to file at #{path} on #{remote.config.hostname}",
43
+ resp.stderr
44
+ end
45
+ end
46
+
47
+ def destroy!
48
+ resp = remote.run "rm #{path}"
49
+ if resp.success?
50
+ Log.info :success, "file at #{path} on #{remote.config.hostname} removed"
51
+ else
52
+ Log.error "failed to remove #{path} on #{remote.config.hostname}",
53
+ resp.stderr
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def remote_file_exists?
60
+ resp = remote.run "[ -f #{path} ] && echo \"exists\""
61
+ resp.success? && resp.stdout.strip == "exists"
62
+ end
63
+
64
+ def create_remote_file
65
+ resp = remote.run "touch #{path} " +
66
+ "&& chmod #{permissions} #{path}"
67
+
68
+ if resp.success?
69
+ Log.info :success, "created file at #{path} on #{remote.config.hostname}"
70
+ else
71
+ Log.error "problem creating file at #{path} on #{remote.config.hostname}",
72
+ resp.stderr
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,111 @@
1
+ require_relative 'remote/file'
2
+
3
+ module Statistrano
4
+
5
+ # a remote is a databag of config specific for an
6
+ # individual target for deployment
7
+ # including it's own ssh connection to it's target server
8
+ class Remote
9
+
10
+ attr_reader :config
11
+
12
+ def initialize config
13
+ @config = config
14
+ raise ArgumentError, "a hostname is required" unless config.hostname
15
+ end
16
+
17
+ def test_connection
18
+ Log.info "testing connection to #{config.hostname}"
19
+
20
+ resp = run 'whoami'
21
+ done
22
+
23
+ if resp.success?
24
+ Log.info "#{config.hostname} says \"Hello #{resp.stdout.strip}\""
25
+ return true
26
+ else
27
+ Log.error "connection failed for #{config.hostname}",
28
+ resp.stderr
29
+ return false
30
+ end
31
+ end
32
+
33
+ def run command
34
+ if config.verbose
35
+ Log.info :"#{config.hostname}", "running cmd: #{command}"
36
+ end
37
+
38
+ session.run command
39
+ end
40
+
41
+ def done
42
+ session.close_session
43
+ end
44
+
45
+ def create_remote_dir path
46
+ unless path[0] == "/"
47
+ raise ArgumentError, "path must be absolute"
48
+ end
49
+
50
+ Log.info "Setting up directory at '#{path}' on #{config.hostname}"
51
+ resp = run "mkdir -p -m #{config.dir_permissions} #{path}"
52
+ unless resp.success?
53
+ Log.error "Unable to create directory '#{path}' on #{config.hostname}",
54
+ resp.stderr
55
+ abort()
56
+ end
57
+ end
58
+
59
+ def rsync_to_remote local_path, remote_path
60
+ local_path = local_path.chomp("/")
61
+ remote_path = remote_path.chomp("/")
62
+
63
+ Log.info "Syncing files from '#{local_path}' to '#{remote_path}' on #{config.hostname}"
64
+
65
+ time_before = Time.now
66
+ resp = Shell.run_local "rsync #{rsync_options} " +
67
+ "-e ssh #{local_path}/ " +
68
+ "#{host_connection}:#{remote_path}/"
69
+ time_after = Time.now
70
+ total_time = (time_after - time_before).round(2)
71
+
72
+ if resp.success?
73
+ Log.info :success, "Files synced to remote on #{config.hostname} in #{total_time}s"
74
+ else
75
+ Log.error "Error syncing files to remote on #{config.hostname}",
76
+ resp.stderr
77
+ end
78
+
79
+ resp
80
+ end
81
+
82
+ private
83
+
84
+ def session
85
+ @_ssh_session ||= HereOrThere::Remote.session ssh_options
86
+ end
87
+
88
+ def ssh_options
89
+ ssh_options = { hostname: config.hostname }
90
+ [ :user, :password, :keys, :forward_agent ].each do |key|
91
+ ssh_options[key] = config.public_send(key) if config.public_send(key)
92
+ end
93
+
94
+ return ssh_options
95
+ end
96
+
97
+ def host_connection
98
+ config.user ? "#{config.user}@#{config.hostname}" : config.hostname
99
+ end
100
+
101
+ def rsync_options
102
+ dir_perms = Util::FilePermissions.new( config.dir_permissions ).to_chmod
103
+ file_perms = Util::FilePermissions.new( config.file_permissions ).to_chmod
104
+
105
+ "#{config.rsync_flags} --chmod=" +
106
+ "Du=#{dir_perms.user},Dg=#{dir_perms.group},Do=#{dir_perms.others}," +
107
+ "Fu=#{file_perms.user},Fg=#{file_perms.group},Fo=#{file_perms.others}"
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,17 @@
1
+ module Statistrano
2
+ module Shell
3
+
4
+ class << self
5
+ include HereOrThere
6
+
7
+ # get input from the command line
8
+ # @return [String]
9
+ def get_input message
10
+ print message
11
+ $stdin.gets.chomp
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ module Statistrano
2
+ module Util
3
+
4
+ class FilePermissions
5
+
6
+ attr_reader :user, :group, :others
7
+
8
+ def initialize int
9
+ @user, @group, @others = int.to_s.chars.to_a
10
+ end
11
+
12
+ def to_chmod
13
+ Struct.new(:user, :group, :others)
14
+ .new( chmod_map(user), chmod_map(group), chmod_map(others) )
15
+ end
16
+
17
+ private
18
+
19
+ def chmod_map num
20
+ {
21
+ "7" => "rwx",
22
+ "6" => "rw",
23
+ "5" => "rx",
24
+ "4" => "r",
25
+ "3" => "wx",
26
+ "2" => "w",
27
+ "1" => "x",
28
+ "0" => "-"
29
+ }.fetch num
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'util/file_permissions'
2
+
3
+ module Statistrano
4
+ module Util
5
+
6
+ class << self
7
+
8
+ def symbolize_hash_keys hash
9
+ hash.inject({}) do |out, (key, value)|
10
+ k = case key
11
+ when String then key.to_sym
12
+ else key
13
+ end
14
+ v = case value
15
+ when Hash then symbolize_hash_keys(value)
16
+ when Array then value.map { |h| symbolize_hash_keys(h) }
17
+ else value
18
+ end
19
+ out[k] = v
20
+ out
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Statistrano
2
+ VERSION = "1.2.0"
3
+ end
@@ -0,0 +1,55 @@
1
+ # stdlib
2
+ require 'json'
3
+ require 'forwardable'
4
+ require 'fileutils'
5
+ require 'benchmark'
6
+
7
+ # libraries
8
+ require 'rainbow'
9
+ require 'rake'
10
+ require 'slugity/extend_string'
11
+ require 'here_or_there'
12
+ require 'asgit'
13
+
14
+ # utility modules
15
+ require_relative 'statistrano/util'
16
+ require_relative 'statistrano/shell'
17
+ require_relative 'statistrano/log'
18
+
19
+ # deployment modules
20
+ require_relative 'statistrano/config'
21
+ require_relative 'statistrano/remote'
22
+ require_relative 'statistrano/deployment'
23
+
24
+
25
+ # DSL for defining deployments of static files
26
+ #
27
+ # == Define a server
28
+ #
29
+ # define_deployment "foo" do |config|
30
+ # config.attribute = value
31
+ # end
32
+ #
33
+ module Statistrano::DSL
34
+
35
+ # Define a deployment
36
+ # @param [String] name of the deployment
37
+ # @param [Symbol] type of deployment
38
+ # @return [Statistrano::Deployment::Base]
39
+ def define_deployment name, type=:base, &block
40
+ deployment = ::Statistrano::Deployment::Strategy.find(type).new( name )
41
+
42
+ if block_given?
43
+ if block.arity == 1
44
+ yield deployment.config
45
+ else
46
+ deployment.config.instance_eval &block
47
+ end
48
+ end
49
+
50
+ return deployment
51
+ end
52
+
53
+ end
54
+
55
+ include Statistrano::DSL