r-train 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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