chake 0.19 → 0.80

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 (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
-