luban 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +37 -0
  6. data/Rakefile +1 -0
  7. data/bin/console +14 -0
  8. data/bin/setup +7 -0
  9. data/exe/luban +3 -0
  10. data/lib/luban/deployment/cli/application/authenticator.rb +106 -0
  11. data/lib/luban/deployment/cli/application/base.rb +179 -0
  12. data/lib/luban/deployment/cli/application/builder.rb +67 -0
  13. data/lib/luban/deployment/cli/application/publisher.rb +215 -0
  14. data/lib/luban/deployment/cli/application/repository.rb +175 -0
  15. data/lib/luban/deployment/cli/application/scm/git.rb +49 -0
  16. data/lib/luban/deployment/cli/application/scm/rsync.rb +47 -0
  17. data/lib/luban/deployment/cli/application.rb +5 -0
  18. data/lib/luban/deployment/cli/command.rb +360 -0
  19. data/lib/luban/deployment/cli/package/binary.rb +241 -0
  20. data/lib/luban/deployment/cli/package/dependency.rb +49 -0
  21. data/lib/luban/deployment/cli/package/dependency_set.rb +71 -0
  22. data/lib/luban/deployment/cli/package/installer/core.rb +98 -0
  23. data/lib/luban/deployment/cli/package/installer/install.rb +330 -0
  24. data/lib/luban/deployment/cli/package/installer/paths.rb +81 -0
  25. data/lib/luban/deployment/cli/package/installer.rb +3 -0
  26. data/lib/luban/deployment/cli/package/service.rb +17 -0
  27. data/lib/luban/deployment/cli/package/worker.rb +43 -0
  28. data/lib/luban/deployment/cli/package.rb +6 -0
  29. data/lib/luban/deployment/cli/project.rb +94 -0
  30. data/lib/luban/deployment/cli.rb +4 -0
  31. data/lib/luban/deployment/configuration/core.rb +67 -0
  32. data/lib/luban/deployment/configuration/filter.rb +54 -0
  33. data/lib/luban/deployment/configuration/question.rb +38 -0
  34. data/lib/luban/deployment/configuration/server.rb +70 -0
  35. data/lib/luban/deployment/configuration/server_set.rb +86 -0
  36. data/lib/luban/deployment/configuration.rb +5 -0
  37. data/lib/luban/deployment/error.rb +5 -0
  38. data/lib/luban/deployment/helpers/configuration.rb +159 -0
  39. data/lib/luban/deployment/helpers/utils.rb +180 -0
  40. data/lib/luban/deployment/helpers.rb +2 -0
  41. data/lib/luban/deployment/packages/bundler.rb +81 -0
  42. data/lib/luban/deployment/packages/git.rb +37 -0
  43. data/lib/luban/deployment/packages/openssl.rb +59 -0
  44. data/lib/luban/deployment/packages/ruby.rb +125 -0
  45. data/lib/luban/deployment/packages/rubygems.rb +89 -0
  46. data/lib/luban/deployment/packages/yaml.rb +33 -0
  47. data/lib/luban/deployment/parameters.rb +160 -0
  48. data/lib/luban/deployment/runner.rb +99 -0
  49. data/lib/luban/deployment/templates/envrc.erb +30 -0
  50. data/lib/luban/deployment/templates/unset_envrc.erb +28 -0
  51. data/lib/luban/deployment/version.rb +5 -0
  52. data/lib/luban/deployment/worker/base.rb +71 -0
  53. data/lib/luban/deployment/worker/controller.rb +11 -0
  54. data/lib/luban/deployment/worker/local.rb +19 -0
  55. data/lib/luban/deployment/worker/remote.rb +55 -0
  56. data/lib/luban/deployment/worker/task.rb +25 -0
  57. data/lib/luban/deployment/worker.rb +4 -0
  58. data/lib/luban/deployment.rb +8 -0
  59. data/lib/luban.rb +4 -0
  60. data/luban.gemspec +29 -0
  61. metadata +174 -0
@@ -0,0 +1,38 @@
1
+ module Luban
2
+ module Deployment
3
+ class Configuration
4
+ class Question
5
+ attr_reader :prompt, :default
6
+
7
+ def initialize(default:, prompt: nil, echo: true)
8
+ @default = default
9
+ @echo = echo
10
+ @prompt = "#{prompt.to_s}: " unless prompt.nil?
11
+ end
12
+
13
+ def echo?; @echo; end
14
+
15
+ def call
16
+ ask_question
17
+ get_response
18
+ end
19
+
20
+ protected
21
+
22
+ def ask_question
23
+ $stdout.print prompt
24
+ end
25
+
26
+ def get_response
27
+ response = if echo?
28
+ $stdin.gets.chomp
29
+ else
30
+ require 'io/console'
31
+ $stdin.noecho(&:gets).chomp.tap{ $stdout.print "\n" }
32
+ end
33
+ response.empty? ? default : response
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ require 'set'
2
+
3
+ module Luban
4
+ module Deployment
5
+ class Configuration
6
+ class Server < SSHKit::Host
7
+ attr_reader :roles
8
+
9
+ def self.create(host, **properties)
10
+ server = host.is_a?(Server) ? host : Server.new(host)
11
+ server.add_properties(properties) unless properties.empty?
12
+ server.ssh_options ||= {}
13
+ server
14
+ end
15
+
16
+ def roles
17
+ self[:roles]
18
+ end
19
+
20
+ def add_roles(roles)
21
+ roles.each { |role| add_role(role) }
22
+ end
23
+ alias_method :roles=, :add_roles
24
+
25
+ def add_role(role)
26
+ roles.add(role.to_sym)
27
+ end
28
+
29
+ def has_role?(role)
30
+ roles.include? role.to_sym
31
+ end
32
+
33
+ def properties
34
+ @properties ||= { :roles => Set.new }
35
+ end
36
+
37
+ def [](key)
38
+ properties[key]
39
+ end
40
+ alias_method :fetch, :[]
41
+
42
+ def []=(key, value)
43
+ if respond_to?("#{key}=")
44
+ send("#{key}=", value)
45
+ else
46
+ pval = properties[key]
47
+ if pval.is_a? Hash and value.is_a? Hash
48
+ pval.merge!(value)
49
+ elsif pval.is_a? Set and value.is_a? Set
50
+ pval.merge(value)
51
+ elsif pval.is_a? Array and value.is_a? Array
52
+ pval.concat value
53
+ else
54
+ properties[key] = value
55
+ end
56
+ end
57
+ end
58
+ alias_method :set, :[]=
59
+
60
+ def add_properties(_properties)
61
+ _properties.each { |k, v| self[k] = v }
62
+ end
63
+
64
+ def primary?
65
+ self[:primary]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,86 @@
1
+ module Luban
2
+ module Deployment
3
+ class Configuration
4
+ class ServerSet
5
+ include Enumerable
6
+
7
+ def initialize
8
+ @servers = { :all => [] }
9
+ end
10
+
11
+ def all
12
+ @servers[:all]
13
+ end
14
+
15
+ def available_roles
16
+ @servers.keys - [:all]
17
+ end
18
+
19
+ def has_server?(server)
20
+ all.find do |s|
21
+ s.user == server.user and
22
+ s.hostname == server.hostname and
23
+ s.port == server.port
24
+ end
25
+ end
26
+
27
+ def add_host(host, **properties)
28
+ new_server = Server.create(host, properties)
29
+ if server = has_server?(new_server)
30
+ server.user = new_server.user unless new_server.user.nil?
31
+ server.port = new_server.port unless new_server.port.nil?
32
+ server.add_properties(properties)
33
+ server
34
+ else
35
+ all << new_server
36
+ new_server.roles.each do |role|
37
+ @servers[role] ||= []
38
+ @servers[role] << new_server
39
+ end
40
+ new_server
41
+ end
42
+ end
43
+
44
+ def add_hosts_for_role(role, hosts, **properties)
45
+ properties_deepcopy = Marshal.dump(properties.merge(:roles => Array(role)))
46
+ Array(hosts).each { |host| add_host(host, Marshal.load(properties_deepcopy)) }
47
+ end
48
+
49
+ def find_by_roles(roles, **opts)
50
+ roles.inject([]) do |result, role|
51
+ result.concat(find_by_role(role, opts))
52
+ end
53
+ end
54
+
55
+ def find_by_role(role, **opts)
56
+ if @servers.has_key?(role)
57
+ opts.inject(@servers[role]) do |_servers, (action, key)|
58
+ send(action, _servers, key)
59
+ end
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def find_primary(role)
66
+ _servers = find_by_role(role)
67
+ _servers.find(&:primary?) || _servers.first
68
+ end
69
+
70
+ def each
71
+ all.each { |server| yield server }
72
+ end
73
+
74
+ protected
75
+
76
+ def exclude(_servers, key)
77
+ _servers.select { |server| !server[key] }
78
+ end
79
+
80
+ def filter(_servers, key)
81
+ _servers.select { |server| server[key] }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'configuration/core'
2
+ require_relative 'configuration/filter'
3
+ require_relative 'configuration/question'
4
+ require_relative 'configuration/server'
5
+ require_relative 'configuration/server_set'
@@ -0,0 +1,5 @@
1
+ module Luban
2
+ module Deployment
3
+ class Error < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,159 @@
1
+ module Luban
2
+ module Deployment
3
+ module Helpers
4
+ module Configuration
5
+ attr_accessor :config
6
+
7
+ def config
8
+ @config ||= Luban::Deployment::Configuration.new
9
+ end
10
+
11
+ def fetch(key, *args, &blk)
12
+ config.fetch(key, *args, &blk)
13
+ end
14
+
15
+ def set(key, *args, &blk)
16
+ config.set(key, *args, &blk)
17
+ end
18
+
19
+ def set_default(key, *args, &blk)
20
+ config.set_default(key, *args, &blk)
21
+ end
22
+
23
+ def ask(key=nil, default: nil, prompt: nil, echo: true)
24
+ config.ask(key, default: default, echo: echo, prompt: prompt)
25
+ end
26
+
27
+ def role(name, hosts, **properties)
28
+ config.role(name, hosts, properties)
29
+ end
30
+
31
+ def server(name, **properties)
32
+ config.server(name, properties)
33
+ end
34
+
35
+ def roles(*names)
36
+ config.roles(*names)
37
+ end
38
+
39
+ def release_roles(*names)
40
+ config.release_roles(*names)
41
+ end
42
+
43
+ def primary(role)
44
+ config.primary(role)
45
+ end
46
+
47
+ def load_configuration_file(config_file, optional: false)
48
+ if File.file?(config_file)
49
+ if error = syntax_error?(config_file)
50
+ abort "Aborted! Syntax errors in configuration file.\n#{error}"
51
+ else
52
+ instance_eval(File.read(config_file))
53
+ end
54
+ else
55
+ unless optional
56
+ abort "Aborted! Configuration file is NOT found: #{config_file}"
57
+ end
58
+ end
59
+ end
60
+
61
+ def syntax_error?(file)
62
+ _stderr = $stderr
63
+ $stderr = StringIO.new('', 'w')
64
+ # ONLY work for MRI Ruby
65
+ RubyVM::InstructionSequence.compile_file(file)
66
+ $stderr.string.chomp.empty? ? false : $stderr.string
67
+ ensure
68
+ $stderr = _stderr
69
+ end
70
+
71
+ def find_template_file(file_name)
72
+ path = find_template_file_by_config_finder(file_name) ||
73
+ find_default_template_file(file_name)
74
+ raise RuntimeError, "Template file is NOT found: #{file_name}." if path.nil?
75
+ path
76
+ end
77
+
78
+ protected
79
+
80
+ def find_template_file_by_config_finder(file_name)
81
+ path = config_finder[:application].find_template_file(file_name) ||
82
+ config_finder[:project].find_template_file(file_name)
83
+ end
84
+
85
+ def find_default_template_file(file_name)
86
+ path = File.expand_path(File.join(File.dirname(__FILE__), '..',
87
+ 'templates', file_name))
88
+ return path if File.file?(path)
89
+ end
90
+
91
+ class Finder
92
+ class Project < Finder
93
+ def base_path; base_path ||= target.work_dir; end
94
+ end
95
+
96
+ class Application < Finder
97
+ def base_path; base_path ||= target.apps_path.join(target.application); end
98
+ end
99
+
100
+ def self.project(target); Project.new(target); end
101
+ def self.application(target); Application.new(target); end
102
+
103
+ attr_reader :target
104
+ attr_reader :base_path
105
+ attr_reader :config_root
106
+ attr_reader :config_file
107
+ attr_reader :config_path
108
+ attr_reader :templates_path
109
+ attr_reader :stage_config_file
110
+ attr_reader :stage_config_path
111
+ attr_reader :stage_templates_path
112
+
113
+ def initialize(target)
114
+ @target = target
115
+ set_config_paths
116
+ end
117
+
118
+ def deployfile; @deployfile ||= 'deploy.rb'; end
119
+ def stagefile; @stagefile ||= "#{target.stage}.rb"; end
120
+
121
+ def load_configuration
122
+ load_general_configuration
123
+ load_stage_configuration
124
+ end
125
+
126
+ def load_general_configuration
127
+ target.load_configuration_file(config_file)
128
+ end
129
+
130
+ def load_stage_configuration
131
+ target.load_configuration_file(stage_config_file)
132
+ if File.directory?(stage_config_path)
133
+ Dir[stage_config_path.join("{packages}/**/*.rb")].each do |file|
134
+ target.load_configuration_file(file)
135
+ end
136
+ end
137
+ end
138
+
139
+ def find_template_file(file_name)
140
+ return file_path if File.file?(file_path = stage_templates_path.join(file_name))
141
+ return file_path if File.file?(file_path = templates_path.join(file_name))
142
+ end
143
+
144
+ protected
145
+
146
+ def set_config_paths
147
+ @config_root = base_path.join('config')
148
+ @config_file = @config_root.join(deployfile)
149
+ @config_path = @config_root.join(deployfile.sub('.rb', ''))
150
+ @templates_path = @config_root.join('templates')
151
+ @stage_config_file = @config_path.join(stagefile)
152
+ @stage_config_path = @config_path.join(target.stage)
153
+ @stage_templates_path = @stage_config_path.join('templates')
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,180 @@
1
+ require "digest/md5"
2
+
3
+ module Luban
4
+ module Deployment
5
+ module Helpers
6
+ module Utils
7
+ LogLevels = %i(fatal error warn info debug trace)
8
+
9
+ attr_reader :backend
10
+
11
+ def check_pass?(type, *args)
12
+ send("#{type}?", *args)
13
+ end
14
+
15
+ def directory?(path)
16
+ test "[ -d #{path} ]"
17
+ end
18
+
19
+ def file?(path)
20
+ test "[ -f #{path} ]"
21
+ end
22
+
23
+ def symlink?(path)
24
+ test "[ -L #{path} ]"
25
+ end
26
+
27
+ def match?(cmd, expect)
28
+ expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
29
+ output = capture(cmd)
30
+ output =~ expect
31
+ end
32
+
33
+ def assure(type, *args)
34
+ unless check_pass?(type, *args)
35
+ if block_given?
36
+ yield
37
+ else
38
+ abort "Aborted! #{type} dependency with #{args.inspect} are not met and no block is given to resolve it."
39
+ end
40
+ end
41
+ end
42
+
43
+ def assure_dirs(*dirs)
44
+ dirs.each { |dir| assure(:directory, dir) { mkdir(dir) } }
45
+ end
46
+
47
+ def assure_symlink(source_path, target_path)
48
+ unless symlink?(target_path) and readlink(target_path) == source_path.to_s
49
+ ln(source_path, target_path)
50
+ end
51
+ end
52
+
53
+ def mkdir(*opts, path)
54
+ execute(:mkdir, '-p', *opts, path)
55
+ end
56
+
57
+ def rm(*opts, path)
58
+ execute(:rm, '-fr', *opts, path)
59
+ end
60
+ alias_method :rmdir, :rm
61
+
62
+ def chmod(*opts, path)
63
+ execute(:chmod, '-R', *opts, path)
64
+ end
65
+
66
+ def ln(*opts, source_path, target_path)
67
+ execute(:ln, '-nfs', *opts, source_path, target_path)
68
+ end
69
+
70
+ def mv(*opts, source_path, target_path)
71
+ execute(:mv, *opts, source_path, target_path)
72
+ end
73
+
74
+ def cp(*opts, source_path, target_path)
75
+ execute(*opts, source_path, target_path)
76
+ end
77
+
78
+ def readlink(source_file)
79
+ capture("$(type -p readlink greadlink|head -1) #{source_file}")
80
+ end
81
+
82
+ def md5_for_file(file)
83
+ capture(:cat, "#{file} 2>/dev/null | openssl md5")
84
+ end
85
+
86
+ def sudo(*args)
87
+ execute(:sudo, *args)
88
+ end
89
+
90
+ def os_name
91
+ @os_name ||= capture("uname -s")
92
+ end
93
+
94
+ def os_release
95
+ @os_release ||= capture("uname -r")
96
+ end
97
+
98
+ def hardware_name
99
+ @hardware_name ||= capture("uname -m")
100
+ end
101
+
102
+ def user_home
103
+ @user_home ||= capture("eval echo ~")
104
+ end
105
+
106
+ def url_exists?(download_url)
107
+ # Sent HEAD request to avoid downloading the file contents
108
+ test("curl -s -L -I -o /dev/null -f #{download_url}")
109
+
110
+ # Other effective ways to check url existence with curl
111
+
112
+ # In case HEAD request is refused,
113
+ # only the first byte of the file is requested
114
+ # test("curl -s -L -o /dev/null -f -r 0-0 #{download_url}")
115
+
116
+ # Alternatively, http code (200) can be validated
117
+ # capture("curl -s -L -I -o /dev/null -w '%{http_code}' #{download_url}") == '200'
118
+ end
119
+
120
+ def upload_by_template(file_to_upload:, template_file:, auto_revision: false, **opts)
121
+ template = File.read(template_file)
122
+
123
+ if auto_revision and file?(file_to_upload)
124
+ revision = Digest::MD5.hexdigest(template)
125
+ return if revision_match?(file_to_upload, revision)
126
+ end
127
+
128
+ require 'erb'
129
+ context = opts[:binding] || binding
130
+ upload!(StringIO.new(ERB.new(template, nil, '<>').result(context)), file_to_upload)
131
+ yield file_to_upload if block_given?
132
+ end
133
+
134
+ def revision_match?(file_to_upload, revision)
135
+ match?("grep \"Revision: \" #{file_to_upload}", revision)
136
+ end
137
+
138
+ [:test, :make, :within, :with, :as, :execute,
139
+ :upload!, :download!].each do |cmd|
140
+ define_method(cmd) do |*args, &blk|
141
+ backend.send(__method__, *args, &blk)
142
+ end
143
+ end
144
+
145
+ def capture(*args, &blk)
146
+ backend.capture(*args, &blk).chomp
147
+ end
148
+
149
+ def md5_matched?(file_path, md5)
150
+ file?(file_path) and md5 == md5_for_file(file_path)
151
+ end
152
+
153
+ #def execute(*args, &blk)
154
+ # if args.last.is_a?(::Hash)
155
+ # args.last.merge(verbosity: SSHKit::Logger::DEBUG)
156
+ # else
157
+ # args.push(verbosity: SSHKit::Logger::DEBUG)
158
+ # end
159
+ # backend.execute(*args, &blk)
160
+ #end
161
+
162
+ LogLevels.each do |cmd|
163
+ define_method(cmd) { |msg| backend.send(__method__, "[#{hostname}] #{msg}") }
164
+ end
165
+
166
+ def host
167
+ @host ||= backend.host
168
+ end
169
+
170
+ def hostname
171
+ @hostname ||= host.hostname
172
+ end
173
+
174
+ def method_missing(sym, *args, &blk)
175
+ backend.respond_to?(sym) ? backend.send(sym, *args, &blk) : super
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'helpers/configuration'
2
+ require_relative 'helpers/utils'
@@ -0,0 +1,81 @@
1
+ module Luban
2
+ module Deployment
3
+ module Packages
4
+ class Bundler < Luban::Deployment::Package::Binary
5
+ protected
6
+
7
+ def setup_install_tasks
8
+ super
9
+ commands[:install].switch :install_doc, "Install Bundler document"
10
+ end
11
+
12
+ class Installer < Luban::Deployment::Package::Installer
13
+ def install_doc?
14
+ task.opts.install_doc
15
+ end
16
+
17
+ def package_version
18
+ parent.task.opts.bundler || super
19
+ end
20
+
21
+ def package_path
22
+ parent.package_path
23
+ end
24
+
25
+ def install_path
26
+ parent.install_path
27
+ end
28
+
29
+ def bundler_executable
30
+ @bundler_executable ||= bin_path.join('bundle')
31
+ end
32
+
33
+ def gem_executable
34
+ parent.gem_executable
35
+ end
36
+
37
+ def src_file_extname
38
+ @src_file_extname ||= 'gem'
39
+ end
40
+
41
+ def source_repo
42
+ #@source_repo ||= "http://production.cf.rubygems.org"
43
+ @source_repo ||= "https://rubygems.org"
44
+ end
45
+
46
+ def source_url_root
47
+ @source_url_root ||= "downloads"
48
+ end
49
+
50
+ def installed?
51
+ return false unless file?(bundler_executable)
52
+ match?("#{bundler_executable} -v", package_version)
53
+ end
54
+
55
+ protected
56
+
57
+ def validate
58
+ if parent.nil?
59
+ abort "Aborted! Parent package for Bundler MUST be provided."
60
+ end
61
+ unless parent.is_a?(Ruby::Installer)
62
+ abort "Aborted! Parent package for Bundler MUST be an instance of #{Ruby::Installer.name}"
63
+ end
64
+ end
65
+
66
+ def uncompress_package; end
67
+ def configure_package; end
68
+ def make_package; end
69
+ def update_binstubs!; end
70
+
71
+ def install_package!
72
+ install_opts = ['--local']
73
+ install_opts << "--no-document" unless install_doc?
74
+ test("#{gem_executable} uninstall bundler -a -x -I >> #{install_log_file_path} 2>&1") and
75
+ test("#{gem_executable} install #{install_opts.join(' ')} #{src_file_path} >> #{install_log_file_path} 2>&1")
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,37 @@
1
+ module Luban
2
+ module Deployment
3
+ module Packages
4
+ class Git < Luban::Deployment::Package::Binary
5
+ apply_to :all do
6
+ before_install do
7
+ depend_on 'openssl', version: '1.0.2e'
8
+ end
9
+ end
10
+
11
+ class Installer < Luban::Deployment::Package::Installer
12
+ def git_executable
13
+ @git_executable ||= bin_path.join('git')
14
+ end
15
+
16
+ def source_repo
17
+ @source_repo ||= "https://www.kernel.org"
18
+ end
19
+
20
+ def source_url_root
21
+ @source_url_root ||= "pub/software/scm/git"
22
+ end
23
+
24
+ def installed?
25
+ return false unless file?(git_executable)
26
+ pattern = "version #{package_major_version}"
27
+ match?("#{git_executable} --version", pattern)
28
+ end
29
+
30
+ def with_openssl_dir(dir)
31
+ @configure_opts << "--with-openssl=#{dir}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end