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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a21e10d9f04926c86f5594321e1beef3d71c314ed61649ff1476cf4d632a4ee
4
- data.tar.gz: 2c0d14e4ae4cc96111e2fea95fde8affc3f89686867910c459007bb06c110bd2
3
+ metadata.gz: b9700e07ca7ddd76297069ea6c62e00bb4ed82b87f48f643be7ba41e560e9bd4
4
+ data.tar.gz: 94f6354276aa9ebad26add90b234a5478f9f1ba1157fe34a69297354a29523ea
5
5
  SHA512:
6
- metadata.gz: f9aa3d0be0cb45cf030333df24ca40431e1c387c7a3d0a086dacb03c5fa2c64f7e15bd02f98e1a5b70374950d986be90dd6757b0faeb460baa614992cca477af
7
- data.tar.gz: 95e16299ded0673a3c7fd85db9e04defe7c0d04db6c51459024cd75badd8b3a1672485f8d7f0e40af7e21c6aaa9964e153676a390173d6e7e3a068939905df4d
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
  ```
@@ -39,4 +39,5 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency "pry"
40
40
  spec.add_development_dependency "rake"
41
41
  spec.add_development_dependency "rspec"
42
+ spec.add_development_dependency "yard"
42
43
  end
@@ -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
- tar.new_entry do |e|
59
- entry(e, f, path)
60
- tar.write_header e
61
- tar.write_data content(f) if File.file? f
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 = path_name(file, path)
68
- ent.size = size(file) if File.file? file
95
+ ent.pathname = file.gsub(%r{#{File.dirname(path)}/}, "")
96
+ ent.size = size(file)
69
97
  ent.mode = mode(file)
70
- ent.filetype = file_type(file)
71
- ent.atime = timestamp
72
- ent.mtime = timestamp
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") unless File.directory? file
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
@@ -17,7 +17,7 @@
17
17
  module Kitchen
18
18
  module Transport
19
19
  class Express
20
- VERSION = "1.1.0"
20
+ VERSION = "1.2.0"
21
21
  end
22
22
  end
23
23
  end
@@ -21,15 +21,37 @@ require_relative "express/archiver"
21
21
 
22
22
  module Kitchen
23
23
  module Transport
24
- LOG_PREFIX = "EXPRESS"
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 = Concurrent::FixedThreadPool.new([processed_locals.length, 10].min)
103
+ pool, exceptions = thread_pool(processed_locals)
63
104
  processed_locals.each do |local|
64
- pool.post { transfer(local, remote, session.options) }
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
- def valid_remote_requirements?
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
- private
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.1.0
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-01-29 00:00:00.000000000 Z
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: