r-train 0.9.1

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +45 -0
  4. data/.travis.yml +12 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +201 -0
  7. data/README.md +137 -0
  8. data/Rakefile +39 -0
  9. data/lib/train.rb +100 -0
  10. data/lib/train/errors.rb +23 -0
  11. data/lib/train/extras.rb +15 -0
  12. data/lib/train/extras/command_wrapper.rb +105 -0
  13. data/lib/train/extras/file_common.rb +131 -0
  14. data/lib/train/extras/linux_file.rb +74 -0
  15. data/lib/train/extras/linux_lsb.rb +60 -0
  16. data/lib/train/extras/os_common.rb +131 -0
  17. data/lib/train/extras/os_detect_darwin.rb +32 -0
  18. data/lib/train/extras/os_detect_linux.rb +126 -0
  19. data/lib/train/extras/os_detect_unix.rb +77 -0
  20. data/lib/train/extras/os_detect_windows.rb +73 -0
  21. data/lib/train/extras/stat.rb +92 -0
  22. data/lib/train/extras/windows_file.rb +85 -0
  23. data/lib/train/options.rb +80 -0
  24. data/lib/train/plugins.rb +40 -0
  25. data/lib/train/plugins/base_connection.rb +86 -0
  26. data/lib/train/plugins/transport.rb +49 -0
  27. data/lib/train/transports/docker.rb +102 -0
  28. data/lib/train/transports/local.rb +52 -0
  29. data/lib/train/transports/local_file.rb +77 -0
  30. data/lib/train/transports/local_os.rb +51 -0
  31. data/lib/train/transports/mock.rb +125 -0
  32. data/lib/train/transports/ssh.rb +163 -0
  33. data/lib/train/transports/ssh_connection.rb +216 -0
  34. data/lib/train/transports/winrm.rb +187 -0
  35. data/lib/train/transports/winrm_connection.rb +258 -0
  36. data/lib/train/version.rb +7 -0
  37. data/test/integration/.kitchen.yml +43 -0
  38. data/test/integration/Berksfile +3 -0
  39. data/test/integration/bootstrap.sh +17 -0
  40. data/test/integration/chefignore +1 -0
  41. data/test/integration/cookbooks/test/metadata.rb +1 -0
  42. data/test/integration/cookbooks/test/recipes/default.rb +101 -0
  43. data/test/integration/docker_run.rb +153 -0
  44. data/test/integration/docker_test.rb +24 -0
  45. data/test/integration/docker_test_container.rb +24 -0
  46. data/test/integration/helper.rb +58 -0
  47. data/test/integration/sudo/nopasswd.rb +16 -0
  48. data/test/integration/sudo/passwd.rb +21 -0
  49. data/test/integration/sudo/run_as.rb +12 -0
  50. data/test/integration/test-runner.yaml +24 -0
  51. data/test/integration/test_local.rb +19 -0
  52. data/test/integration/test_ssh.rb +24 -0
  53. data/test/integration/tests/path_block_device_test.rb +74 -0
  54. data/test/integration/tests/path_character_device_test.rb +74 -0
  55. data/test/integration/tests/path_file_test.rb +79 -0
  56. data/test/integration/tests/path_folder_test.rb +88 -0
  57. data/test/integration/tests/path_missing_test.rb +77 -0
  58. data/test/integration/tests/path_pipe_test.rb +78 -0
  59. data/test/integration/tests/path_symlink_test.rb +83 -0
  60. data/test/integration/tests/run_command_test.rb +28 -0
  61. data/test/unit/extras/command_wrapper_test.rb +41 -0
  62. data/test/unit/extras/file_common_test.rb +133 -0
  63. data/test/unit/extras/linux_file_test.rb +98 -0
  64. data/test/unit/extras/os_common_test.rb +258 -0
  65. data/test/unit/extras/stat_test.rb +105 -0
  66. data/test/unit/helper.rb +6 -0
  67. data/test/unit/plugins/connection_test.rb +44 -0
  68. data/test/unit/plugins/transport_test.rb +111 -0
  69. data/test/unit/plugins_test.rb +22 -0
  70. data/test/unit/train_test.rb +132 -0
  71. data/test/unit/transports/local_file_test.rb +112 -0
  72. data/test/unit/transports/local_test.rb +73 -0
  73. data/test/unit/transports/mock_test.rb +76 -0
  74. data/test/unit/transports/ssh_test.rb +95 -0
  75. data/test/unit/version_test.rb +8 -0
  76. data/train.gemspec +32 -0
  77. metadata +299 -0
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+
5
+ module Train
6
+ VERSION = '0.9.1'
7
+ end
@@ -0,0 +1,43 @@
1
+ ---
2
+ driver:
3
+ name: vagrant
4
+
5
+ provisioner:
6
+ name: chef_solo
7
+ data_path: ../../.
8
+
9
+ platforms:
10
+ - name: centos-7.1
11
+ - name: centos-6.7
12
+ - name: centos-6.7-i386
13
+ - name: centos-5.11
14
+ - name: centos-5.11-i386
15
+ - name: debian-6.0.10
16
+ - name: debian-6.0.10-i386
17
+ - name: debian-7.8
18
+ - name: debian-7.8-i386
19
+ - name: debian-8.1
20
+ - name: debian-8.1-i386
21
+ - name: fedora-21
22
+ - name: fedora-21-i386
23
+ - name: fedora-22
24
+ - name: freebsd-9.3
25
+ - name: freebsd-10.2
26
+ - name: opensuse-13.2-x86_64
27
+ - name: opensuse-13.2-i386
28
+ - name: ubuntu-14.04
29
+ - name: ubuntu-14.04-i386
30
+ - name: ubuntu-12.04
31
+ - name: ubuntu-12.04-i386
32
+ - name: ubuntu-10.04
33
+ - name: ubuntu-10.04-i386
34
+
35
+ suites:
36
+ - name: default
37
+ run_list:
38
+ - recipe[sudo]
39
+ - recipe[test]
40
+ attributes:
41
+ authorization:
42
+ sudo:
43
+ include_sudoers_d: true
@@ -0,0 +1,3 @@
1
+ source 'https://supermarket.chef.io'
2
+ cookbook 'sudo', '~> 2.7.2'
3
+ cookbook 'test', path: 'cookbooks/test'
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ test ! -e /tmp/folder && \
3
+ mkdir /tmp/folder
4
+ chmod 0567 /tmp/folder
5
+
6
+ echo -n 'hello world' > /tmp/file
7
+ test ! -e /tmp/symlink && \
8
+ ln -s /tmp/file /tmp/symlink
9
+ chmod 0777 /tmp/symlink
10
+ chmod 0765 /tmp/file
11
+
12
+ test ! -e /tmp/pipe && \
13
+ mkfifo /tmp/pipe
14
+
15
+ test ! -e /tmp/block_device && \
16
+ mknod /tmp/block_device b 7 7
17
+ chmod 0666 /tmp/block_device
@@ -0,0 +1 @@
1
+ .kitchen
@@ -0,0 +1 @@
1
+ name 'test'
@@ -0,0 +1,101 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ #
4
+ # Helper recipe to create create a few files in the operating
5
+ # systems, which the runner will test against.
6
+ # It also initializes the runner inside the machines
7
+ # and makes sure all dependencies are ready to go.
8
+ #
9
+ # Finally (for now), it actually executes the all tests with
10
+ # the local execution backend
11
+
12
+ gid = 'root'
13
+ gid = 'wheel' if node['platform_family'] == 'freebsd'
14
+
15
+ file '/tmp/file' do
16
+ mode '0765'
17
+ owner 'root'
18
+ group gid
19
+ content 'hello world'
20
+ end
21
+
22
+ directory '/tmp/folder' do
23
+ mode '0567'
24
+ owner 'root'
25
+ group gid
26
+ end
27
+
28
+ link '/tmp/symlink'do
29
+ to '/tmp/file'
30
+ owner 'root'
31
+ group gid
32
+ mode '0777'
33
+ end
34
+
35
+ execute 'create pipe/fifo' do
36
+ command 'mkfifo /tmp/pipe'
37
+ not_if 'test -e /tmp/pipe'
38
+ end
39
+
40
+ execute 'create block_device' do
41
+ command "mknod /tmp/block_device b 7 7 && chmod 0666 /tmp/block_device && chown root:#{gid} /tmp/block_device"
42
+ not_if 'test -e /tmp/block_device'
43
+ end
44
+
45
+ # prepare ssh for backend
46
+ execute 'create ssh key' do
47
+ command 'ssh-keygen -t rsa -b 2048 -f /root/.ssh/id_rsa -N ""'
48
+ not_if 'test -e /root/.ssh/id_rsa'
49
+ end
50
+
51
+ execute 'add ssh key to vagrant user' do
52
+ command 'cat /root/.ssh/id_rsa.pub >> /home/vagrant/.ssh/authorized_keys'
53
+ end
54
+
55
+ execute 'test ssh connection' do
56
+ command 'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa vagrant@localhost "echo 1"'
57
+ end
58
+
59
+ # prepare a few users
60
+ %w{ nopasswd passwd nosudo }.each do |name|
61
+ user name do
62
+ password '$1$7MCNTXPI$r./jqCEoVlLlByYKSL3sZ.'
63
+ manage_home true
64
+ end
65
+ end
66
+
67
+ %w{nopasswd vagrant}.each do |name|
68
+ sudo name do
69
+ user '%'+name
70
+ nopasswd true
71
+ end
72
+ end
73
+
74
+ sudo 'passwd' do
75
+ user 'passwd'
76
+ nopasswd false
77
+ end
78
+
79
+ # execute tests
80
+ execute 'bundle install' do
81
+ command '/opt/chef/embedded/bin/bundle install'
82
+ cwd '/tmp/kitchen/data'
83
+ end
84
+
85
+ execute 'run local tests' do
86
+ command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_local.rb test/integration/tests/*_test.rb'
87
+ cwd '/tmp/kitchen/data'
88
+ end
89
+
90
+ execute 'run ssh tests' do
91
+ command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_ssh.rb test/integration/tests/*_test.rb'
92
+ cwd '/tmp/kitchen/data'
93
+ end
94
+
95
+ %w{passwd nopasswd}.each do |name|
96
+ execute "run local sudo tests as #{name}" do
97
+ command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/#{name}.rb"
98
+ cwd '/tmp/kitchen/data'
99
+ user name
100
+ end
101
+ end
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+
4
+ require 'docker'
5
+ require 'yaml'
6
+ require 'concurrent'
7
+
8
+ class DockerRunner
9
+ def initialize(conf_path = nil)
10
+ @conf_path = conf_path || ENV['config']
11
+ unless File.file?(@conf_path)
12
+ fail "Can't find configuration in #{@conf_path}"
13
+ end
14
+
15
+ @conf = YAML.load_file(@conf_path)
16
+ if @conf.nil? or @conf.empty?
17
+ fail "Can't read coniguration in #{@conf_path}"
18
+ end
19
+ if @conf['images'].nil?
20
+ fail "You must configure test images in your #{@conf_path}"
21
+ end
22
+
23
+ @images = docker_images_by_tag
24
+ @image_pull_tickets = Concurrent::Semaphore.new(2)
25
+ @docker_run_tickets = Concurrent::Semaphore.new(5)
26
+ end
27
+
28
+ def run_all(&block)
29
+ fail 'You must provide a block for run_all' unless block_given?
30
+
31
+ promises = @conf['images'].map do |id|
32
+ run_on_target(id, &block)
33
+ end
34
+
35
+ # wait for all tests to be finished
36
+ sleep(0.1) until promises.all?(&:fulfilled?)
37
+
38
+ # return resulting values
39
+ promises.map(&:value)
40
+ end
41
+
42
+ def run_on_target(name, &block)
43
+ pr = Concurrent::Promise.new {
44
+ begin
45
+ container = start_container(name)
46
+ res = block.call(name, container)
47
+ # special rescue block to handle not implemented error
48
+ rescue NotImplementedError => err
49
+ raise err.message
50
+ end
51
+ # always stop the container
52
+ stop_container(container)
53
+ res
54
+ }.execute
55
+
56
+ # failure handling
57
+ pr.rescue do |err|
58
+ msg = "\033[31;1m#{err.message}\033[0m"
59
+ puts msg
60
+ msg + "\n" + err.backtrace.join("\n")
61
+ end
62
+ end
63
+
64
+ def provision_image(image, prov, files)
65
+ tries ||= 3
66
+ return image if prov['script'].nil?
67
+ path = File.join(File.dirname(@conf_path), prov['script'])
68
+ unless File.file?(path)
69
+ puts "Can't find script file #{path}"
70
+ return image
71
+ end
72
+ puts " script #{path}"
73
+ dst = "/bootstrap#{files.length}.sh"
74
+ files.push(dst)
75
+ image.insert_local('localPath' => path, 'outputPath' => dst)
76
+ rescue StandardError => _
77
+ retry unless (tries -= 1).zero?
78
+ end
79
+
80
+ def bootstrap_image(name, image)
81
+ files = []
82
+ provisions = Array(@conf['provision'])
83
+ puts "--> provision docker #{name}" unless provisions.empty?
84
+ provisions.each do |prov|
85
+ image = provision_image(image, prov, files)
86
+ end
87
+ [image, files]
88
+ end
89
+
90
+ def start_container(name, version = nil)
91
+ unless name.include?(':')
92
+ version ||= 'latest'
93
+ name = "#{name}:#{version}"
94
+ end
95
+ puts "--> schedule docker #{name}"
96
+
97
+ image = @images[name]
98
+ if image.nil?
99
+ puts "\033[35;1m--> pull docker images #{name} "\
100
+ "(this may take a while)\033[0m"
101
+
102
+ @image_pull_tickets.acquire(1)
103
+ puts "... start pull image #{name}"
104
+ image = Docker::Image.create('fromImage' => name)
105
+ @image_pull_tickets.release(1)
106
+
107
+ unless image.nil?
108
+ puts "\033[35;1m--> pull docker images finished for #{name}\033[0m"
109
+ end
110
+ end
111
+
112
+ fail "Can't find nor pull docker image #{name}" if image.nil?
113
+
114
+ @docker_run_tickets.acquire(1)
115
+
116
+ image, scripts = bootstrap_image(name, image)
117
+
118
+ puts "--> start docker #{name}"
119
+ container = Docker::Container.create(
120
+ 'Cmd' => %w{sleep 3600},
121
+ 'Image' => image.id,
122
+ 'OpenStdin' => true,
123
+ )
124
+ container.start
125
+
126
+ scripts.each do |script|
127
+ container.exec(%w{chmod +x}.push(script))
128
+ container.exec(%w{sh -c}.push(script))
129
+ end
130
+
131
+ container
132
+ end
133
+
134
+ def stop_container(container)
135
+ @docker_run_tickets.release(1)
136
+ puts "--> killrm docker #{container.id}"
137
+ container.kill
138
+ container.delete(force: true)
139
+ end
140
+
141
+ private
142
+
143
+ # get all docker image tags
144
+ def docker_images_by_tag
145
+ images = {}
146
+ Docker::Image.all.map do |img|
147
+ Array(img.info['RepoTags']).each do |tag|
148
+ images[tag] = img
149
+ end
150
+ end
151
+ images
152
+ end
153
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ require_relative 'docker_run'
3
+ tests = ARGV
4
+
5
+ def test_container(container, tests)
6
+ puts "--> run test on docker #{container.id}"
7
+ pid = Process.fork do
8
+ ENV['CONTAINER'] = container.id
9
+ require_relative 'docker_test_container.rb'
10
+ Process.exit
11
+ end
12
+
13
+ _, status = Process.waitpid2(pid)
14
+ status.exitstatus == 0
15
+ end
16
+
17
+ results = DockerRunner.new.run_all do |name, container|
18
+ status = test_container(container, tests)
19
+ status ? nil : "Failed to run tests on #{name}"
20
+ end
21
+
22
+ failures = results.compact
23
+ failures.each { |f| puts "\033[31;1m#{f}\033[0m\n\n" }
24
+ failures.empty? or fail 'Test failures'
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+
4
+ require 'train'
5
+ require_relative 'helper'
6
+
7
+ container_id = ENV['CONTAINER'] or
8
+ fail 'You must provide a container ID via CONTAINER env'
9
+
10
+ tests = ARGV
11
+ puts ['Running tests:', tests].flatten.join("\n- ")
12
+ puts ''
13
+
14
+ backends = {}
15
+ backends[:docker] = proc { |*args|
16
+ opt = Train.target_config({ host: container_id })
17
+ Train.create('docker', opt).connection(args[0])
18
+ }
19
+
20
+ backends.each do |type, get_backend|
21
+ tests.each do |test|
22
+ instance_eval(File.read(test), test, 1)
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+
7
+ # Tests configuration:
8
+ module Test
9
+ class << self
10
+ # MTime tracks the maximum range of modification time in seconds.
11
+ # i.e. MTime == 60*60*1 is 1 hour of modification time range,
12
+ # which translates to a modification time range of:
13
+ # [ now-1hour, now ]
14
+ def mtime
15
+ 60 * 60 * 24 * 1
16
+ end
17
+
18
+ def dup(o)
19
+ Marshal.load(Marshal.dump(o))
20
+ end
21
+
22
+ def root_group(os)
23
+ if os[:family] == 'freebsd'
24
+ 'wheel'
25
+ else
26
+ 'root'
27
+ end
28
+ end
29
+
30
+ def selinux_label(backend, path = nil)
31
+ return nil if backend.class.to_s =~ /docker/i
32
+
33
+ os = backend.os
34
+ labels = {}
35
+
36
+ h = {}
37
+ h.default = Hash.new(nil)
38
+ h['redhat'] = {}
39
+ h['redhat'].default = 'unconfined_u:object_r:user_tmp_t:s0'
40
+ h['redhat']['5.11'] = 'user_u:object_r:tmp_t'
41
+ h['centos'] = h['fedora'] = h['redhat']
42
+ labels.default = dup(h)
43
+
44
+ h['redhat'].default = 'unconfined_u:object_r:tmp_t:s0'
45
+ labels['/tmp/block_device'] = dup(h)
46
+
47
+ h = {}
48
+ h.default = Hash.new(nil)
49
+ h['redhat'] = {}
50
+ h['redhat'].default = 'system_u:object_r:null_device_t:s0'
51
+ h['redhat']['5.11'] = 'system_u:object_r:null_device_t'
52
+ h['centos'] = h['fedora'] = h['redhat']
53
+ labels['/dev/null'] = dup(h)
54
+
55
+ labels[path][os[:family]][os[:release]]
56
+ end
57
+ end
58
+ end