statistrano 1.2.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/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