kitchen-transport-express 1.1.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +1 -1
- data/kitchen-transport-express.gemspec +1 -0
- data/lib/kitchen/transport/express/archiver.rb +57 -30
- data/lib/kitchen/transport/express/version.rb +1 -1
- data/lib/kitchen/transport/express_ssh.rb +91 -12
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9700e07ca7ddd76297069ea6c62e00bb4ed82b87f48f643be7ba41e560e9bd4
|
4
|
+
data.tar.gz: 94f6354276aa9ebad26add90b234a5478f9f1ba1157fe34a69297354a29523ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6989457b293f6a59dabef6bea15684548769fbac53ded1413effadcf44bdcf797afe0560fba452378453d18ba3a5f88b16768ae6eb30876ca24cc1756fe0afa
|
7
|
+
data.tar.gz: 9f2a64a0eb918cdb14980d46b3e1f2dfbcb59d4631329784fd41d0835b1c353da2a36900483f0113067bec16b2fb277994e5d95d17af8823a2ce447ea36da899
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# kitchen-transport-express CHANGELOG
|
2
2
|
|
3
|
+
## 1.2.0
|
4
|
+
* feat: 🥅 add error handling to the thread pool
|
5
|
+
* feat: 📝🎨 add YARD tags and cleaned up class namespaces and private methods
|
6
|
+
|
3
7
|
## 1.1.0
|
4
8
|
* feat: ⚡️ threaded execution of the upload and extract phase
|
5
9
|
* fix: 🩹 add binary mode to archiver when reading a file
|
data/README.md
CHANGED
@@ -23,7 +23,7 @@ transport:
|
|
23
23
|
Verify that everything has loaded correctly with `kitchen list`. You should see `ExpressSsh` as the transport.
|
24
24
|
|
25
25
|
```bash
|
26
|
-
> kitchen list
|
26
|
+
> kitchen list
|
27
27
|
Instance Driver Provisioner Verifier Transport Last Action Last Error
|
28
28
|
default-linux Oci ChefInfra Inspec ExpressSsh <Not Created> <None>
|
29
29
|
```
|
@@ -19,32 +19,47 @@ require "ffi-libarchive"
|
|
19
19
|
module Kitchen
|
20
20
|
module Transport
|
21
21
|
class Express
|
22
|
+
# Mixin module that provides methods for creating and extracting archives locally and on the remote host.
|
23
|
+
#
|
24
|
+
# @author Justin Steele <justin.steele@oracle.com>
|
22
25
|
module Archiver
|
26
|
+
# Creates the archive locally in the Kitchen cache location
|
27
|
+
#
|
28
|
+
# @param path [String] the path of the top-level directory to be arvhied
|
29
|
+
# @return [String] the name of the archive
|
23
30
|
def archive(path)
|
24
31
|
archive_basename = ::File.basename(path) + ".tgz"
|
25
32
|
archive_full_name = ::File.join(::File.dirname(path), archive_basename)
|
26
33
|
|
27
34
|
file_count = ::Dir.glob(::File.join(path, "**/*")).size
|
28
|
-
logger.debug("[#{LOG_PREFIX}] #{path} contains #{file_count} files.")
|
35
|
+
logger.debug("[#{Express::LOG_PREFIX}] #{path} contains #{file_count} files.")
|
29
36
|
create_archive(path, archive_full_name)
|
30
37
|
archive_full_name
|
31
38
|
end
|
32
39
|
|
40
|
+
# Extracts the archive on the remote host
|
41
|
+
#
|
42
|
+
# @param session [Net::SSH::Connection::Session] The SSH session used to connect to the remote host and execute
|
43
|
+
# the extract and cleanup commands
|
33
44
|
def extract(session, local, remote)
|
34
45
|
return unless local.match(/.*\.tgz/)
|
35
46
|
|
36
47
|
archive_basename = File.basename(local)
|
37
|
-
logger.debug("[#{LOG_PREFIX}] Extracting #{::File.join(remote, archive_basename)}")
|
48
|
+
logger.debug("[#{Express::LOG_PREFIX}] Extracting #{::File.join(remote, archive_basename)}")
|
38
49
|
session.open_channel do |channel|
|
39
50
|
channel.request_pty
|
40
|
-
channel.exec("tar -xzf #{::File.join(remote, archive_basename)} -C #{remote}")
|
41
|
-
channel.exec("rm -f #{File.join(remote, archive_basename)}")
|
51
|
+
channel.exec("tar -xzf #{::File.join(remote, archive_basename)} -C #{remote} && rm -f #{File.join(remote, archive_basename)}")
|
42
52
|
end
|
43
53
|
session.loop
|
44
54
|
end
|
45
55
|
|
46
56
|
private
|
47
57
|
|
58
|
+
# Creats a archive of the directory provided
|
59
|
+
#
|
60
|
+
# @param path [String] the path to the directory that will be archived
|
61
|
+
# @param archive_path [String] the fully qualified path to the archive that will be created
|
62
|
+
# @api private
|
48
63
|
def create_archive(path, archive_path)
|
49
64
|
Archive.write_open_filename(archive_path, Archive::COMPRESSION_GZIP,
|
50
65
|
Archive::FORMAT_TAR_PAX_RESTRICTED) do |tar|
|
@@ -52,54 +67,66 @@ module Kitchen
|
|
52
67
|
end
|
53
68
|
end
|
54
69
|
|
70
|
+
# Appends the content of each item in the expanded directory path
|
71
|
+
#
|
72
|
+
# @param tar [Archive::Writer] the instance of the archive class
|
73
|
+
# @param path [String] the path to the directory that will be archived
|
74
|
+
# @api private
|
55
75
|
def write_content(tar, path)
|
56
76
|
all_files = Dir.glob("#{path}/**/*")
|
57
77
|
all_files.each do |f|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
78
|
+
if File.file? f
|
79
|
+
tar.new_entry do |e|
|
80
|
+
entry(e, f, path)
|
81
|
+
tar.write_header e
|
82
|
+
tar.write_data content(f)
|
83
|
+
end
|
62
84
|
end
|
63
85
|
end
|
64
86
|
end
|
65
87
|
|
88
|
+
# Creates the entry in the Archive for each item
|
89
|
+
#
|
90
|
+
# @param ent [Archive::Entry] the current entry being added to the archive
|
91
|
+
# @param file [String] the current file or directory being added to the archive
|
92
|
+
# @param path [String] the path to the directory being archived
|
93
|
+
# @api private
|
66
94
|
def entry(ent, file, path)
|
67
|
-
ent.pathname =
|
68
|
-
ent.size = size(file)
|
95
|
+
ent.pathname = file.gsub(%r{#{File.dirname(path)}/}, "")
|
96
|
+
ent.size = size(file)
|
69
97
|
ent.mode = mode(file)
|
70
|
-
ent.filetype =
|
71
|
-
ent.atime =
|
72
|
-
ent.mtime =
|
73
|
-
end
|
74
|
-
|
75
|
-
def path_name(file, path)
|
76
|
-
file.gsub(%r{#{File.dirname(path)}/}, "")
|
77
|
-
end
|
78
|
-
|
79
|
-
def file_type(file)
|
80
|
-
if File.file? file
|
81
|
-
Archive::Entry::FILE
|
82
|
-
elsif File.directory? file
|
83
|
-
Archive::Entry::DIRECTORY
|
84
|
-
end
|
98
|
+
ent.filetype = Archive::Entry::FILE
|
99
|
+
ent.atime = Time.now.to_i
|
100
|
+
ent.mtime = Time.now.to_i
|
85
101
|
end
|
86
102
|
|
103
|
+
# The content of the file in binary format. Directories have no content.
|
104
|
+
#
|
105
|
+
# @param file [String] the path to the file
|
106
|
+
# @return [String] the content of the file
|
107
|
+
# @api private
|
87
108
|
def content(file)
|
88
|
-
File.read(file, mode: "rb")
|
109
|
+
File.read(file, mode: "rb")
|
89
110
|
end
|
90
111
|
|
112
|
+
# The size of the file. Directories have no size.
|
113
|
+
#
|
114
|
+
# @param file [String] the path to the file
|
115
|
+
# @return [Integer] the size of the file
|
116
|
+
# @api private
|
91
117
|
def size(file)
|
92
118
|
content(file).size
|
93
119
|
end
|
94
120
|
|
121
|
+
# The file permissions of the file
|
122
|
+
#
|
123
|
+
# @param file [String] the path to the file or directory
|
124
|
+
# @return [Integer] the mode of the file or directory
|
125
|
+
# @api private
|
95
126
|
def mode(file)
|
96
127
|
f = File.stat(file)
|
97
128
|
f.mode
|
98
129
|
end
|
99
|
-
|
100
|
-
def timestamp
|
101
|
-
Time.now.to_i
|
102
|
-
end
|
103
130
|
end
|
104
131
|
end
|
105
132
|
end
|
@@ -21,15 +21,37 @@ require_relative "express/archiver"
|
|
21
21
|
|
22
22
|
module Kitchen
|
23
23
|
module Transport
|
24
|
-
|
24
|
+
# Kitchen Transport Express
|
25
|
+
#
|
26
|
+
# @author Justin Steele <justin.steele@oracle.com>
|
27
|
+
class Express
|
28
|
+
# A constant that gets prepended to debugger messages
|
29
|
+
LOG_PREFIX = "EXPRESS"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Express SSH Transport Error class
|
33
|
+
#
|
34
|
+
# @author Justin Steele <justin.steele@oracle.com>
|
35
|
+
class ExpressFailed < StandardError
|
36
|
+
def initialize(message, exit_code = nil)
|
37
|
+
super("#{Express::LOG_PREFIX} file transfer failed. #{message}.")
|
38
|
+
end
|
39
|
+
end
|
25
40
|
|
41
|
+
# Express SSH Transport plugin for Test Kitchen
|
42
|
+
#
|
43
|
+
# @author Justin Steele <justin.steele@oracle.com>
|
26
44
|
class ExpressSsh < Kitchen::Transport::Ssh
|
27
45
|
kitchen_transport_api_version 1
|
28
46
|
plugin_version Express::VERSION
|
29
47
|
|
48
|
+
# Override the method in the super class to start the connection with our connection class
|
49
|
+
#
|
50
|
+
# @param options [Hash] connection options
|
51
|
+
# @return [Ssh::Connection] an instance of Kitchen::Transport::ExpressSsh::Connection
|
30
52
|
def create_new_connection(options, &block)
|
31
53
|
if @connection
|
32
|
-
logger.debug("[#{LOG_PREFIX}] Shutting previous connection #{@connection}")
|
54
|
+
logger.debug("[#{Express::LOG_PREFIX}] Shutting previous connection #{@connection}")
|
33
55
|
@connection.close
|
34
56
|
end
|
35
57
|
|
@@ -37,10 +59,17 @@ module Kitchen
|
|
37
59
|
@connection = self.class::Connection.new(options, &block)
|
38
60
|
end
|
39
61
|
|
62
|
+
# Determines if the Kitchen instance is attempting a Verify stage
|
63
|
+
#
|
64
|
+
# @param instance [Kitchen::Instance] the instance passed in from Kitchen
|
65
|
+
# @return [Boolean]
|
40
66
|
def verifier_defined?(instance)
|
41
67
|
defined?(Kitchen::Verifier::Inspec) && instance.verifier.is_a?(Kitchen::Verifier::Inspec)
|
42
68
|
end
|
43
69
|
|
70
|
+
# Finalizes the Kitchen config by executing super and parsing the options provided by the kitchen.yml
|
71
|
+
# The only difference here is we layer in our ssh options so the verifier can use our transport
|
72
|
+
# (see Kitchen::Transport::Ssh#finalize_config!)
|
44
73
|
def finalize_config!(instance)
|
45
74
|
super.tap do
|
46
75
|
if verifier_defined?(instance)
|
@@ -51,33 +80,73 @@ module Kitchen
|
|
51
80
|
end
|
52
81
|
end
|
53
82
|
|
83
|
+
# This connection instance overrides the default behavior of the upload method in
|
84
|
+
# Kitchen::Transport::Ssh::Connection to provide the zip-and-ship style transfer of files
|
85
|
+
# to the kitchen instances. All other behavior from the superclass is default.
|
86
|
+
#
|
87
|
+
# @author Justin Steele <justin.steele@oracle.com>
|
54
88
|
class Connection < Kitchen::Transport::Ssh::Connection
|
55
89
|
include Express::Archiver
|
56
90
|
|
91
|
+
# (see Kitchen::Transport::Base::Connection#upload)
|
92
|
+
# Overrides the upload method in Kitchen::Transport::Ssh::Connection
|
93
|
+
# The special sauce here is that we create threaded executions of uploading our archives
|
94
|
+
#
|
95
|
+
# @param locals [Array] the top-level list of directories and files to be transfered
|
96
|
+
# @param remote [String] the remote directory config[:kitchen_root]
|
97
|
+
# @raise [ExpressFailed] if any of the threads raised an exception
|
98
|
+
# rubocop: disable Metrics/MethodLength
|
57
99
|
def upload(locals, remote)
|
58
|
-
return super unless valid_remote_requirements?
|
100
|
+
return super unless valid_remote_requirements?(remote)
|
59
101
|
|
60
|
-
execute("mkdir -p #{remote}")
|
61
102
|
processed_locals = process_locals(locals)
|
62
|
-
pool =
|
103
|
+
pool, exceptions = thread_pool(processed_locals)
|
63
104
|
processed_locals.each do |local|
|
64
|
-
pool.post
|
105
|
+
pool.post do
|
106
|
+
transfer(local, remote, session.options)
|
107
|
+
rescue => e
|
108
|
+
exceptions << e.cause
|
109
|
+
end
|
65
110
|
end
|
66
111
|
pool.shutdown
|
67
112
|
pool.wait_for_termination
|
113
|
+
|
114
|
+
raise ExpressFailed, exceptions.pop unless exceptions.empty?
|
68
115
|
end
|
116
|
+
# rubocop: enable Metrics/MethodLength
|
69
117
|
|
70
|
-
|
118
|
+
private
|
119
|
+
|
120
|
+
# Creates the thread pool and exceptions queue
|
121
|
+
#
|
122
|
+
# @param processed_locals [Array] list of files and archives to be uploaded
|
123
|
+
# @return [Array(Concurrent::FixedThreadPool, Queue)]
|
124
|
+
# @api private
|
125
|
+
def thread_pool(processed_locals)
|
126
|
+
[Concurrent::FixedThreadPool.new([processed_locals.length, 10].min), Queue.new]
|
127
|
+
end
|
128
|
+
|
129
|
+
# Ensures the remote host has the minimum-required executables to extract the archives.
|
130
|
+
#
|
131
|
+
# @param remote [String] the remote directory config[:kitchen_root]
|
132
|
+
# @return [Boolean]
|
133
|
+
# @api private
|
134
|
+
def valid_remote_requirements?(remote)
|
71
135
|
execute("(which tar && which gzip) > /dev/null")
|
136
|
+
execute("mkdir -p #{remote}")
|
72
137
|
true
|
73
138
|
rescue => e
|
74
|
-
logger.debug("[#{LOG_PREFIX}] Requirements not met on remote host for Express transport.")
|
75
|
-
logger.debug(e)
|
139
|
+
logger.debug("[#{Express::LOG_PREFIX}] Requirements not met on remote host for Express transport.")
|
140
|
+
logger.debug("[#{Express::LOG_PREFIX}] #{e}")
|
76
141
|
false
|
77
142
|
end
|
78
143
|
|
79
|
-
|
80
|
-
|
144
|
+
# Builds an array of files we want to ship. If the top-level item is a directory, archive it and
|
145
|
+
# add the archive name to the array.
|
146
|
+
#
|
147
|
+
# @param locals [Array] the top-level list of directories and files to be transfered
|
148
|
+
# @return [Array] the paths to the files and archives that will be transferred
|
149
|
+
# @api private
|
81
150
|
def process_locals(locals)
|
82
151
|
processed_locals = []
|
83
152
|
Array(locals).each do |local|
|
@@ -91,12 +160,22 @@ module Kitchen
|
|
91
160
|
processed_locals
|
92
161
|
end
|
93
162
|
|
163
|
+
# Uploads the archives or files to the remote host.
|
164
|
+
#
|
165
|
+
# @param local [String] a single top-level item from the upload method
|
166
|
+
# @param remote [String] path to remote destination
|
167
|
+
# @param opts [Hash] the ssh options that came in from the Kitchen instance
|
168
|
+
# @raise [StandardError] if the files could not be uploaded successfully
|
169
|
+
# @api private
|
94
170
|
def transfer(local, remote, opts = {})
|
95
|
-
logger.debug("[#{LOG_PREFIX}] Transferring #{local} to #{remote}")
|
171
|
+
logger.debug("[#{Express::LOG_PREFIX}] Transferring #{local} to #{remote}")
|
96
172
|
|
97
173
|
Net::SSH.start(session.host, opts[:user], **opts) do |ssh|
|
98
174
|
ssh.scp.upload!(local, remote, opts)
|
99
175
|
extract(ssh, local, remote)
|
176
|
+
rescue Net::SCP::Error => ex
|
177
|
+
logger.debug("[#{Express::LOG_PREFIX}] upload failed with #{ex.message.strip}")
|
178
|
+
raise "(#{ex.message.strip})"
|
100
179
|
end
|
101
180
|
end
|
102
181
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kitchen-transport-express
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Steele
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: test-kitchen
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yard
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
description: A Test Kitchen Transport plugin that streamlines the file transfer phase
|
126
140
|
to Linux hosts.
|
127
141
|
email:
|