chake 0.19 → 0.80

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ackrc +1 -0
  3. data/.gitignore +2 -0
  4. data/.gitlab-ci.yml +21 -9
  5. data/.manifest +63 -0
  6. data/.rubocop.yml +53 -0
  7. data/.rubocop_todo.yml +40 -0
  8. data/ChangeLog.md +36 -0
  9. data/README.chef.md +70 -0
  10. data/README.itamae.md +58 -0
  11. data/README.md +124 -85
  12. data/README.shell.md +30 -0
  13. data/Rakefile +40 -13
  14. data/bin/chake +12 -1
  15. data/chake.gemspec +16 -16
  16. data/examples/test/.ssh_config +4 -0
  17. data/examples/test/Rakefile +1 -1
  18. data/examples/test/Vagrantfile +6 -0
  19. data/examples/test/config.rb +4 -4
  20. data/examples/test/cookbooks/basics/recipes/default.rb +1 -0
  21. data/examples/test/cookbooks/example/files/default/test +1 -0
  22. data/examples/test/cookbooks/example/files/{host-homer → host-lemur}/test.asc +0 -0
  23. data/lib/chake.rb +92 -168
  24. data/lib/chake/bootstrap/{01_debian.sh → chef/01_debian.sh} +0 -0
  25. data/lib/chake/bootstrap/{99_unsupported.sh → chef/99_unsupported.sh} +0 -0
  26. data/lib/chake/config.rb +16 -0
  27. data/lib/chake/config_manager.rb +93 -0
  28. data/lib/chake/config_manager/chef.rb +35 -0
  29. data/lib/chake/config_manager/itamae.rb +58 -0
  30. data/lib/chake/config_manager/shell.rb +34 -0
  31. data/lib/chake/config_manager/skel/chef/Rakefile +1 -0
  32. data/lib/chake/config_manager/skel/chef/config.rb +4 -0
  33. data/lib/chake/config_manager/skel/chef/cookbooks/basics/recipes/default.rb +1 -0
  34. data/lib/chake/config_manager/skel/chef/nodes.yaml +3 -0
  35. data/lib/chake/config_manager/skel/itamae/Rakefile +1 -0
  36. data/lib/chake/config_manager/skel/itamae/cookbooks/basics/default.rb +1 -0
  37. data/lib/chake/config_manager/skel/itamae/nodes.yaml +3 -0
  38. data/lib/chake/config_manager/skel/itamae/roles/basic.rb +1 -0
  39. data/lib/chake/config_manager/skel/shell/Rakefile +1 -0
  40. data/lib/chake/config_manager/skel/shell/nodes.yaml +3 -0
  41. data/lib/chake/connection.rb +83 -0
  42. data/lib/chake/{backend → connection}/local.rb +2 -8
  43. data/lib/chake/{backend → connection}/ssh.rb +6 -14
  44. data/lib/chake/node.rb +49 -29
  45. data/lib/chake/readline.rb +6 -10
  46. data/lib/chake/version.rb +1 -1
  47. data/man/Rakefile +27 -14
  48. data/man/readme2man.sed +5 -5
  49. data/spec/chake/backend/local_spec.rb +5 -6
  50. data/spec/chake/backend/ssh_spec.rb +8 -10
  51. data/spec/chake/backend_spec.rb +1 -2
  52. data/spec/chake/config_manager/chef_spec.rb +38 -0
  53. data/spec/chake/config_manager/itamae_spec.rb +69 -0
  54. data/spec/chake/config_manager/shell_spec.rb +54 -0
  55. data/spec/chake/config_manager_spec.rb +24 -0
  56. data/spec/chake/node_spec.rb +38 -15
  57. data/spec/spec_helper.rb +23 -19
  58. metadata +61 -14
  59. data/lib/chake/backend.rb +0 -78
@@ -0,0 +1,16 @@
1
+ require 'chake/node'
2
+
3
+ module Chake
4
+ class << self
5
+ attr_accessor :nodes
6
+ end
7
+ end
8
+
9
+ nodes_file = ENV['CHAKE_NODES'] || 'nodes.yaml'
10
+ nodes_directory = ENV['CHAKE_NODES_D'] || 'nodes.d'
11
+ node_data = File.exist?(nodes_file) && YAML.load_file(nodes_file) || {}
12
+ Dir.glob(File.join(nodes_directory, '*.yaml')).sort.each do |f|
13
+ node_data.merge!(YAML.load_file(f))
14
+ end
15
+
16
+ Chake.nodes = node_data.map { |node, data| Chake::Node.new(node, data) }.reject(&:skip?).uniq(&:hostname)
@@ -0,0 +1,93 @@
1
+ require 'pathname'
2
+
3
+ module Chake
4
+ class ConfigManager
5
+ attr_reader :node
6
+
7
+ def initialize(node)
8
+ @node = node
9
+ end
10
+
11
+ def converge; end
12
+
13
+ def apply(config); end
14
+
15
+ def path
16
+ "/var/tmp/#{name}.#{node.username}"
17
+ end
18
+
19
+ def name
20
+ self.class.short_name
21
+ end
22
+
23
+ def to_s
24
+ name
25
+ end
26
+
27
+ def bootstrap_steps
28
+ base = File.join(File.absolute_path(File.dirname(__FILE__)), 'bootstrap')
29
+ steps = Dir[File.join(base, '*.sh')] + Dir[File.join(base, name, '*.sh')]
30
+ steps.sort_by { |f| File.basename(f) }
31
+ end
32
+
33
+ def needs_bootstrap?
34
+ true
35
+ end
36
+
37
+ def needs_upload?
38
+ true
39
+ end
40
+
41
+ def self.short_name
42
+ name.split('::').last.downcase
43
+ end
44
+
45
+ def self.priority(new_prioriry = nil)
46
+ @priority ||= new_prioriry || 50
47
+ end
48
+
49
+ def self.inherited(klass)
50
+ super
51
+ @subclasses ||= []
52
+ @subclasses << klass
53
+ end
54
+
55
+ def self.get(node)
56
+ available = @subclasses.sort_by(&:priority)
57
+ manager = available.find { |c| c.short_name == node.data['config_manager'] }
58
+ manager ||= available.find { |c| c.accept?(node) }
59
+ raise ArgumentError, "Can't find configuration manager class for node #{node.hostname}. Available: #{available}.join(', ')}" unless manager
60
+
61
+ manager.new(node)
62
+ end
63
+
64
+ def self.accept?(_node)
65
+ false
66
+ end
67
+
68
+ def self.all
69
+ @subclasses
70
+ end
71
+
72
+ def self.init
73
+ skel = Pathname(__FILE__).parent / 'config_manager' / 'skel' / short_name
74
+ skel.glob('**/*').each do |source|
75
+ target = source.relative_path_from(skel)
76
+ if target.exist?
77
+ puts "exists: #{target}"
78
+ else
79
+ if source.directory?
80
+ FileUtils.mkdir_p target
81
+ else
82
+ FileUtils.cp source, target
83
+ end
84
+ puts "create: #{target}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ Dir["#{File.dirname(__FILE__)}/config_manager/*.rb"].sort.each do |f|
92
+ require f
93
+ end
@@ -0,0 +1,35 @@
1
+ require 'chake/config'
2
+ require 'chake/tmpdir'
3
+
4
+ module Chake
5
+ class ConfigManager
6
+ class Chef < ConfigManager
7
+ CONFIG = ENV['CHAKE_CHEF_CONFIG'] || 'config.rb'
8
+
9
+ def converge
10
+ node.run_as_root "sh -c 'rm -f #{node.path}/nodes/*.json && chef-solo -c #{node.path}/#{CONFIG} #{logging} -j #{json_config}'"
11
+ end
12
+
13
+ def apply(config)
14
+ node.run_as_root "sh -c 'rm -f #{node.path}/nodes/*.json && chef-solo -c #{node.path}/#{CONFIG} #{logging} -j #{json_config} --override-runlist recipe[#{config}]'"
15
+ end
16
+
17
+ priority 99
18
+
19
+ def self.accept?(_node)
20
+ true # this is the default, but after everything else
21
+ end
22
+
23
+ private
24
+
25
+ def json_config
26
+ parts = [node.path, Chake.tmpdir, "#{node.hostname}.json"].compact
27
+ File.join(parts)
28
+ end
29
+
30
+ def logging
31
+ node.silent && '-l fatal' || ''
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,58 @@
1
+ require 'shellwords'
2
+ require 'chake/config'
3
+ require 'chake/tmpdir'
4
+
5
+ module Chake
6
+ class ConfigManager
7
+ class Itamae < ConfigManager
8
+ def converge
9
+ run_itamae(*node.data['itamae'])
10
+ end
11
+
12
+ def apply(config)
13
+ run_itamae(config)
14
+ end
15
+
16
+ def needs_bootstrap?
17
+ false
18
+ end
19
+
20
+ def needs_upload?
21
+ false
22
+ end
23
+
24
+ def self.accept?(node)
25
+ node.data.key?('itamae')
26
+ end
27
+
28
+ private
29
+
30
+ def run_itamae(*recipes)
31
+ cmd = ['itamae']
32
+ case node.connection
33
+ when Chake::Connection::Ssh
34
+ cmd << 'ssh' << "--user=#{node.username}" << "--host=#{node.hostname}"
35
+ cmd += ssh_config
36
+ when Chake::Connection::Local
37
+ cmd << 'local'
38
+ else
39
+ raise NotImplementedError, "Connection type #{node.connection.class} not supported for itamee"
40
+ end
41
+ cmd << "--node-json=#{json_config}"
42
+ cmd += recipes
43
+ node.log("$ #{cmd.join(' ')}")
44
+ io = IO.popen(cmd, 'r', err: %i[child out])
45
+ node.connection.read_output(io)
46
+ end
47
+
48
+ def json_config
49
+ File.join(Chake.tmpdir, "#{node.hostname}.json")
50
+ end
51
+
52
+ def ssh_config
53
+ ssh_config = node.connection.send(:ssh_config_file) # FIXME
54
+ File.exist?(ssh_config) ? ["--ssh-config=#{ssh_config}"] : []
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ require 'shellwords'
2
+ require 'chake/config'
3
+
4
+ module Chake
5
+ class ConfigManager
6
+ class Shell < ConfigManager
7
+ def converge
8
+ commands = node.data['shell'].join(' && ')
9
+ node.run_as_root sh(commands)
10
+ end
11
+
12
+ def apply(config)
13
+ node.run_as_root sh(config)
14
+ end
15
+
16
+ def self.accept?(node)
17
+ node.data.key?('shell')
18
+ end
19
+
20
+ private
21
+
22
+ def sh(command)
23
+ if node.path
24
+ command = "cd #{node.path} && " + command
25
+ end
26
+ if node.silent
27
+ "sh -ec '#{command}' >/dev/null"
28
+ else
29
+ "sh -xec '#{command}'"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1 @@
1
+ require 'chake'
@@ -0,0 +1,4 @@
1
+ root = __dir__
2
+ file_cache_path "#{root}/cache"
3
+ cookbook_path "#{root}/cookbooks"
4
+ role_path "#{root}/config/roles"
@@ -0,0 +1,3 @@
1
+ host1.mycompany.com:
2
+ run_list:
3
+ - recipe[basics]
@@ -0,0 +1 @@
1
+ require 'chake'
@@ -0,0 +1 @@
1
+ package 'openssh-server'
@@ -0,0 +1,3 @@
1
+ host1.mycompany.com:
2
+ itamae:
3
+ - roles/basic.rb
@@ -0,0 +1 @@
1
+ include_recipe '../cookbooks/basics'
@@ -0,0 +1 @@
1
+ require 'chake'
@@ -0,0 +1,3 @@
1
+ host1.mycompany.com:
2
+ shell:
3
+ - echo "HELLO WORLD"
@@ -0,0 +1,83 @@
1
+ module Chake
2
+ Connection = Struct.new(:node) do
3
+ class CommandFailed < RuntimeError
4
+ end
5
+
6
+ def scp
7
+ ['scp']
8
+ end
9
+
10
+ def scp_dest
11
+ ''
12
+ end
13
+
14
+ def rsync
15
+ ['rsync']
16
+ end
17
+
18
+ def rsync_dest
19
+ "#{node.path}/"
20
+ end
21
+
22
+ def run(cmd)
23
+ node.log('$ %<command>s' % { command: cmd })
24
+ io = IO.popen(command_runner + ['/bin/sh'], 'w+', err: %i[child out])
25
+ io.write(cmd)
26
+ io.close_write
27
+ read_output(io)
28
+ end
29
+
30
+ def read_output(io)
31
+ io.each_line do |line|
32
+ node.log(line.gsub(/\s*$/, ''))
33
+ end
34
+ io.close
35
+ if $CHILD_STATUS
36
+ status = $CHILD_STATUS.exitstatus
37
+ if status != 0
38
+ raise CommandFailed, [node.hostname, 'FAILED with exit status %<status>d' % { status: status }].join(': ')
39
+ end
40
+ end
41
+ end
42
+
43
+ def run_shell
44
+ system(*shell_command)
45
+ end
46
+
47
+ def run_as_root(cmd)
48
+ if node.remote_username == 'root'
49
+ run(cmd)
50
+ else
51
+ run("sudo #{cmd}")
52
+ end
53
+ end
54
+
55
+ def to_s
56
+ self.class.connection_name
57
+ end
58
+
59
+ def skip?
60
+ false
61
+ end
62
+
63
+ def self.connection_name
64
+ name.split('::').last.downcase
65
+ end
66
+
67
+ def self.inherited(subclass)
68
+ super
69
+ @connections ||= []
70
+ @connections << subclass
71
+ end
72
+
73
+ def self.get(name)
74
+ connection = @connections.find { |b| b.connection_name == name }
75
+ raise ArgumentError, "Invalid connection name: #{name}" unless connection
76
+
77
+ connection
78
+ end
79
+ end
80
+ end
81
+
82
+ require 'chake/connection/ssh'
83
+ require 'chake/connection/local'
@@ -1,11 +1,8 @@
1
1
  require 'socket'
2
2
 
3
3
  module Chake
4
-
5
- class Backend
6
-
7
- class Local < Backend
8
-
4
+ class Connection
5
+ class Local < Connection
9
6
  def command_runner
10
7
  ['sh', '-c']
11
8
  end
@@ -17,9 +14,6 @@ module Chake
17
14
  def skip?
18
15
  node.hostname != Socket.gethostname
19
16
  end
20
-
21
17
  end
22
-
23
18
  end
24
-
25
19
  end
@@ -1,15 +1,12 @@
1
1
  module Chake
2
-
3
- class Backend
4
-
5
- class Ssh < Backend
6
-
2
+ class Connection
3
+ class Ssh < Connection
7
4
  def scp
8
5
  ['scp', ssh_config, scp_options].flatten.compact
9
6
  end
10
7
 
11
8
  def scp_dest
12
- ssh_target + ':'
9
+ "#{ssh_target}:"
13
10
  end
14
11
 
15
12
  def rsync
@@ -17,7 +14,7 @@ module Chake
17
14
  end
18
15
 
19
16
  def rsync_dest
20
- [ssh_target, node.path + '/'].join(':')
17
+ [ssh_target, "#{node.path}/"].join(':')
21
18
  end
22
19
 
23
20
  def command_runner
@@ -35,11 +32,9 @@ module Chake
35
32
  begin
36
33
  ssh_command = 'ssh'
37
34
  if File.exist?(ssh_config_file)
38
- ssh_command += ' -F ' + ssh_config_file
39
- end
40
- if node.port
41
- ssh_command += ' -p ' + node.port.to_s
35
+ ssh_command += " -F #{ssh_config_file}"
42
36
  end
37
+ ssh_command += " -p #{node.port}" if node.port
43
38
  if ssh_command == 'ssh'
44
39
  []
45
40
  else
@@ -71,9 +66,6 @@ module Chake
71
66
  def scp_options
72
67
  node.port && ['-P', node.port.to_s] || []
73
68
  end
74
-
75
69
  end
76
-
77
70
  end
78
-
79
71
  end
@@ -2,58 +2,78 @@ require 'uri'
2
2
  require 'etc'
3
3
  require 'forwardable'
4
4
 
5
- require 'chake/backend'
5
+ require 'chake/connection'
6
+ require 'chake/config_manager'
6
7
 
7
8
  module Chake
8
-
9
9
  class Node
10
-
11
10
  extend Forwardable
12
11
 
13
- attr_reader :hostname
14
- attr_reader :port
15
- attr_reader :username
16
- attr_reader :remote_username
17
- attr_reader :path
18
- attr_reader :data
12
+ attr_reader :hostname, :port, :username, :remote_username, :data
13
+
14
+ attr_accessor :silent
19
15
 
20
16
  def self.max_node_name_length
21
17
  @max_node_name_length ||= 0
22
18
  end
23
- def self.max_node_name_length=(value)
24
- @max_node_name_length = value
19
+
20
+ class << self
21
+ attr_writer :max_node_name_length
25
22
  end
26
23
 
27
24
  def initialize(hostname, data = {})
28
- uri = URI.parse(hostname)
29
- if !uri.host && ((!uri.scheme && uri.path) || (uri.scheme && uri.opaque))
30
- uri = URI.parse("ssh://#{hostname}")
31
- end
32
- if uri.path && uri.path.empty?
33
- uri.path = nil
34
- end
35
-
36
- @backend_name = uri.scheme
37
-
25
+ uri = parse_uri(hostname)
26
+ @connection_name = uri.scheme
38
27
  @hostname = uri.host
39
28
  @port = uri.port
40
29
  @username = uri.user || Etc.getpwuid.name
41
30
  @remote_username = uri.user
42
- @path = uri.path || "/var/tmp/chef.#{username}"
31
+ @path = uri.path
43
32
  @data = data
33
+ set_max_node_length
34
+ end
35
+
36
+ def connection
37
+ @connection ||= Chake::Connection.get(@connection_name).new(self)
38
+ end
39
+
40
+ def_delegators :connection, :run, :run_as_root, :run_shell, :rsync, :rsync_dest, :scp, :scp_dest, :skip?
44
41
 
45
- if @hostname.length > self.class.max_node_name_length
46
- self.class.max_node_name_length = @hostname.length
42
+ def config_manager
43
+ @config_manager ||= Chake::ConfigManager.get(self)
44
+ end
45
+
46
+ def_delegators :config_manager, :converge, :apply, :path, :bootstrap_steps, :needs_bootstrap?, :needs_upload?
47
+
48
+ def path
49
+ @path ||= config_manager.path
50
+ end
51
+
52
+ def log(msg)
53
+ return if silent
54
+
55
+ puts("%#{Node.max_node_name_length}<host>s: %<msg>s\n" % { host: hostname, msg: msg })
56
+ end
57
+
58
+ private
59
+
60
+ def parse_uri(hostname)
61
+ uri = URI.parse(hostname)
62
+ if incomplete_uri(uri)
63
+ uri = URI.parse("ssh://#{hostname}")
47
64
  end
65
+ uri.path = nil if uri.path.empty?
66
+ uri
48
67
  end
49
68
 
50
- def backend
51
- @backend ||= Chake::Backend.get(@backend_name).new(self)
69
+ def incomplete_uri(uri)
70
+ !uri.host && ((!uri.scheme && uri.path) || (uri.scheme && uri.opaque))
52
71
  end
53
72
 
54
- def_delegators :backend, :run, :run_as_root, :run_shell, :rsync, :rsync_dest, :scp, :scp_dest, :skip?
73
+ def set_max_node_length
74
+ return if @hostname.length <= self.class.max_node_name_length
55
75
 
76
+ self.class.max_node_name_length = @hostname.length
77
+ end
56
78
  end
57
-
58
79
  end
59
-