train 0.12.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +71 -0
  3. data/CHANGELOG.md +308 -0
  4. data/Gemfile +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +156 -0
  7. data/Rakefile +148 -0
  8. data/lib/train.rb +117 -0
  9. data/lib/train/errors.rb +23 -0
  10. data/lib/train/extras.rb +17 -0
  11. data/lib/train/extras/command_wrapper.rb +148 -0
  12. data/lib/train/extras/file_aix.rb +20 -0
  13. data/lib/train/extras/file_common.rb +161 -0
  14. data/lib/train/extras/file_linux.rb +16 -0
  15. data/lib/train/extras/file_unix.rb +79 -0
  16. data/lib/train/extras/file_windows.rb +91 -0
  17. data/lib/train/extras/linux_lsb.rb +60 -0
  18. data/lib/train/extras/os_common.rb +136 -0
  19. data/lib/train/extras/os_detect_darwin.rb +32 -0
  20. data/lib/train/extras/os_detect_linux.rb +148 -0
  21. data/lib/train/extras/os_detect_unix.rb +99 -0
  22. data/lib/train/extras/os_detect_windows.rb +57 -0
  23. data/lib/train/extras/stat.rb +133 -0
  24. data/lib/train/options.rb +80 -0
  25. data/lib/train/plugins.rb +40 -0
  26. data/lib/train/plugins/base_connection.rb +86 -0
  27. data/lib/train/plugins/transport.rb +49 -0
  28. data/lib/train/transports/docker.rb +103 -0
  29. data/lib/train/transports/local.rb +52 -0
  30. data/lib/train/transports/local_file.rb +90 -0
  31. data/lib/train/transports/local_os.rb +51 -0
  32. data/lib/train/transports/mock.rb +147 -0
  33. data/lib/train/transports/ssh.rb +163 -0
  34. data/lib/train/transports/ssh_connection.rb +225 -0
  35. data/lib/train/transports/winrm.rb +184 -0
  36. data/lib/train/transports/winrm_connection.rb +194 -0
  37. data/lib/train/version.rb +7 -0
  38. data/test/integration/.kitchen.yml +43 -0
  39. data/test/integration/Berksfile +3 -0
  40. data/test/integration/bootstrap.sh +17 -0
  41. data/test/integration/chefignore +1 -0
  42. data/test/integration/cookbooks/test/metadata.rb +1 -0
  43. data/test/integration/cookbooks/test/recipes/default.rb +100 -0
  44. data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
  45. data/test/integration/docker_run.rb +153 -0
  46. data/test/integration/docker_test.rb +24 -0
  47. data/test/integration/docker_test_container.rb +24 -0
  48. data/test/integration/helper.rb +61 -0
  49. data/test/integration/sudo/customcommand.rb +15 -0
  50. data/test/integration/sudo/nopasswd.rb +16 -0
  51. data/test/integration/sudo/passwd.rb +21 -0
  52. data/test/integration/sudo/reqtty.rb +17 -0
  53. data/test/integration/sudo/run_as.rb +12 -0
  54. data/test/integration/test-travis-1.yaml +13 -0
  55. data/test/integration/test-travis-2.yaml +13 -0
  56. data/test/integration/test_local.rb +19 -0
  57. data/test/integration/test_ssh.rb +39 -0
  58. data/test/integration/tests/path_block_device_test.rb +74 -0
  59. data/test/integration/tests/path_character_device_test.rb +74 -0
  60. data/test/integration/tests/path_file_test.rb +79 -0
  61. data/test/integration/tests/path_folder_test.rb +90 -0
  62. data/test/integration/tests/path_missing_test.rb +77 -0
  63. data/test/integration/tests/path_pipe_test.rb +78 -0
  64. data/test/integration/tests/path_symlink_test.rb +95 -0
  65. data/test/integration/tests/run_command_test.rb +28 -0
  66. data/test/unit/extras/command_wrapper_test.rb +78 -0
  67. data/test/unit/extras/file_common_test.rb +180 -0
  68. data/test/unit/extras/linux_file_test.rb +167 -0
  69. data/test/unit/extras/os_common_test.rb +269 -0
  70. data/test/unit/extras/os_detect_linux_test.rb +189 -0
  71. data/test/unit/extras/os_detect_windows_test.rb +99 -0
  72. data/test/unit/extras/stat_test.rb +148 -0
  73. data/test/unit/extras/windows_file_test.rb +44 -0
  74. data/test/unit/helper.rb +7 -0
  75. data/test/unit/plugins/connection_test.rb +44 -0
  76. data/test/unit/plugins/transport_test.rb +111 -0
  77. data/test/unit/plugins_test.rb +22 -0
  78. data/test/unit/train_test.rb +156 -0
  79. data/test/unit/transports/local_file_test.rb +184 -0
  80. data/test/unit/transports/local_test.rb +87 -0
  81. data/test/unit/transports/mock_test.rb +87 -0
  82. data/test/unit/transports/ssh_test.rb +109 -0
  83. data/test/unit/version_test.rb +8 -0
  84. data/test/windows/local_test.rb +46 -0
  85. data/test/windows/winrm_test.rb +52 -0
  86. data/train.gemspec +38 -0
  87. metadata +295 -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.12.1'.freeze
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,100 @@
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
+ include_recipe('test::prep_files')
13
+
14
+ # prepare ssh for backend
15
+ execute 'create ssh key' do
16
+ command 'ssh-keygen -t rsa -b 2048 -f /root/.ssh/id_rsa -N ""'
17
+ not_if 'test -e /root/.ssh/id_rsa'
18
+ end
19
+
20
+ execute 'add ssh key to vagrant user' do
21
+ command 'cat /root/.ssh/id_rsa.pub >> /home/vagrant/.ssh/authorized_keys'
22
+ end
23
+
24
+ execute 'test ssh connection' do
25
+ command 'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa vagrant@localhost "echo 1"'
26
+ end
27
+
28
+ # prepare a few users
29
+ %w{ nopasswd passwd nosudo reqtty customcommand }.each do |name|
30
+ user name do
31
+ password '$1$7MCNTXPI$r./jqCEoVlLlByYKSL3sZ.'
32
+ manage_home true
33
+ end
34
+ end
35
+
36
+ %w{nopasswd vagrant}.each do |name|
37
+ sudo name do
38
+ user '%'+name
39
+ nopasswd true
40
+ defaults ['!requiretty']
41
+ end
42
+ end
43
+
44
+ sudo 'passwd' do
45
+ user 'passwd'
46
+ nopasswd false
47
+ defaults ['!requiretty']
48
+ end
49
+
50
+ sudo 'reqtty' do
51
+ user 'reqtty'
52
+ nopasswd true
53
+ defaults ['requiretty']
54
+ end
55
+
56
+ sudo 'customcommand' do
57
+ user 'customcommand'
58
+ nopasswd true
59
+ defaults ['!requiretty']
60
+ end
61
+
62
+ # execute tests
63
+ execute 'bundle install' do
64
+ command '/opt/chef/embedded/bin/bundle install --without integration tools'
65
+ cwd '/tmp/kitchen/data'
66
+ end
67
+
68
+ execute 'run local tests' do
69
+ command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_local.rb test/integration/tests/*_test.rb'
70
+ cwd '/tmp/kitchen/data'
71
+ end
72
+
73
+ execute 'run ssh tests' do
74
+ command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_ssh.rb test/integration/tests/*_test.rb'
75
+ cwd '/tmp/kitchen/data'
76
+ end
77
+
78
+ %w{passwd nopasswd reqtty customcommand}.each do |name|
79
+ execute "run local sudo tests as #{name}" do
80
+ command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/#{name}.rb"
81
+ cwd '/tmp/kitchen/data'
82
+ user name
83
+ end
84
+ end
85
+
86
+ execute 'fix sudoers for reqtty' do
87
+ command 'chef-apply contrib/fixup_requiretty.rb'
88
+ cwd '/tmp/kitchen/data'
89
+ environment(
90
+ 'TRAIN_SUDO_USER' => 'reqtty',
91
+ 'TRAIN_SUDO_VERY_MUCH' => 'yes',
92
+ )
93
+ end
94
+
95
+ # if it's fixed, it should behave like user 'nopasswd'
96
+ execute 'run local sudo tests as reqtty, no longer requiring a tty' do
97
+ command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/nopasswd.rb"
98
+ cwd '/tmp/kitchen/data'
99
+ user 'reqtty'
100
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+ #
5
+ # Helper recipe to create create a few files in the operating
6
+ # systems, which the runner will test against.
7
+
8
+ gid = 'root'
9
+ gid = 'wheel' if node['platform_family'] == 'freebsd'
10
+ gid = 'system' if node['platform_family'] == 'aix'
11
+
12
+ file '/tmp/file' do
13
+ mode '0765'
14
+ owner 'root'
15
+ group gid
16
+ content 'hello world'
17
+ end
18
+
19
+ directory '/tmp/folder' do
20
+ mode '0567'
21
+ owner 'root'
22
+ group gid
23
+ end
24
+
25
+ link '/tmp/symlink' do
26
+ to '/tmp/file'
27
+ owner 'root'
28
+ group gid
29
+ mode '0777'
30
+ end
31
+
32
+ link '/usr/bin/allyourbase' do
33
+ to '/usr/bin/sudo'
34
+ owner 'root'
35
+ group gid
36
+ mode '0777'
37
+ end
38
+
39
+ execute 'create pipe/fifo' do
40
+ command 'mkfifo /tmp/pipe'
41
+ not_if 'test -e /tmp/pipe'
42
+ end
43
+
44
+ execute 'create block_device' do
45
+ command "mknod /tmp/block_device b 7 7 && chmod 0666 /tmp/block_device && chown root:#{gid} /tmp/block_device"
46
+ not_if 'test -e /tmp/block_device'
47
+ 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,61 @@
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
+ case os[:family]
24
+ when 'freebsd'
25
+ 'wheel'
26
+ when 'aix'
27
+ 'system'
28
+ else
29
+ 'root'
30
+ end
31
+ end
32
+
33
+ def selinux_label(backend, path = nil)
34
+ return nil if backend.class.to_s =~ /docker/i
35
+
36
+ os = backend.os
37
+ labels = {}
38
+
39
+ h = {}
40
+ h.default = Hash.new(nil)
41
+ h['redhat'] = {}
42
+ h['redhat'].default = 'unconfined_u:object_r:user_tmp_t:s0'
43
+ h['redhat']['5.11'] = 'user_u:object_r:tmp_t'
44
+ h['centos'] = h['fedora'] = h['redhat']
45
+ labels.default = dup(h)
46
+
47
+ h['redhat'].default = 'unconfined_u:object_r:tmp_t:s0'
48
+ labels['/tmp/block_device'] = dup(h)
49
+
50
+ h = {}
51
+ h.default = Hash.new(nil)
52
+ h['redhat'] = {}
53
+ h['redhat'].default = 'system_u:object_r:null_device_t:s0'
54
+ h['redhat']['5.11'] = 'system_u:object_r:null_device_t'
55
+ h['centos'] = h['fedora'] = h['redhat']
56
+ labels['/dev/null'] = dup(h)
57
+
58
+ labels[path][os[:family]][os[:release]]
59
+ end
60
+ end
61
+ end