kitchen-transport-express 1.1.0 → 1.3.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: b255fcbeed01b423bd2bd8e71069501b2c0b636e3d2313ccdd48f8347106333d
4
+ data.tar.gz: 9696bbf314ccbda4c978cf89963e22005a18d064a359d835ccf033403d46f6b8
5
5
  SHA512:
6
- metadata.gz: f9aa3d0be0cb45cf030333df24ca40431e1c387c7a3d0a086dacb03c5fa2c64f7e15bd02f98e1a5b70374950d986be90dd6757b0faeb460baa614992cca477af
7
- data.tar.gz: 95e16299ded0673a3c7fd85db9e04defe7c0d04db6c51459024cd75badd8b3a1672485f8d7f0e40af7e21c6aaa9964e153676a390173d6e7e3a068939905df4d
6
+ metadata.gz: 548f04230a39e196b59fd952a4a5247bb9ee4c754d88785f61400565d7c0bd8802a1857a3c7896bb7f1a311bb63475c7416b7746768436eee60fb9cfa0b2dcc7
7
+ data.tar.gz: ba37810bfcdafaa6ba2973e6410c35aac5ac57bc0f9102db149829cffadea1043a4a24c509f41801eacbc6347cec41ae66907c0b79d90d95484a8bc3ea4d3707
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # kitchen-transport-express CHANGELOG
2
2
 
3
+ ## 1.3.0
4
+ * chore: 📝 minor updates to method documentation
5
+ * chore: 🔧 add metadata to gemspec
6
+
7
+ ## 1.2.0
8
+ * feat: 🥅 add error handling to the thread pool
9
+ * feat: 📝🎨 add YARD tags and cleaned up class namespaces and private methods
10
+
3
11
  ## 1.1.0
4
12
  * feat: ⚡️ threaded execution of the upload and extract phase
5
13
  * 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
  ```
@@ -18,12 +18,12 @@ lib = File.expand_path("lib", __dir__)
18
18
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
19
19
  require "kitchen/transport/express/version"
20
20
 
21
- Gem::Specification.new do |spec|
21
+ Gem::Specification.new do |spec| # rubocop: disable Metrics/BlockLength
22
22
  spec.name = "kitchen-transport-express"
23
23
  spec.version = Kitchen::Transport::Express::VERSION
24
24
  spec.authors = ["Justin Steele"]
25
25
  spec.email = ["justin.steele@oracle.com"]
26
- spec.summary = %q{Skip the long lines in transport. 15 items or less!}
26
+ spec.summary = %q{Skip the long lines in Kitchen Transport!}
27
27
  spec.description = %q{A Test Kitchen Transport plugin that streamlines the file transfer phase to Linux hosts.}
28
28
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
29
29
  spec.homepage = "https://github.com/justintsteele/kitchen-transport-express.git"
@@ -31,6 +31,13 @@ Gem::Specification.new do |spec|
31
31
  spec.license = "Apache-2.0"
32
32
  spec.require_paths = ["lib"]
33
33
  spec.required_ruby_version = ">= 2.4"
34
+ spec.metadata = {
35
+ "bug_tracker_uri" => "https://github.com/justintsteele/kitchen-transport-express/issues",
36
+ "changelog_uri" => "https://github.com/justintsteele/kitchen-transport-express/blob/main/CHANGELOG.md",
37
+ "documentation_uri" => "https://github.com/justintsteele/kitchen-transport-express/blob/main/README.md",
38
+ "homepage_uri" => "https://github.com/justintsteele/kitchen-transport-express",
39
+ "source_code_uri" => "https://github.com/justintsteele/kitchen-transport-express",
40
+ }
34
41
  spec.add_dependency "test-kitchen"
35
42
  spec.add_dependency "ffi-libarchive"
36
43
  spec.add_dependency "concurrent-ruby"
@@ -39,4 +46,5 @@ Gem::Specification.new do |spec|
39
46
  spec.add_development_dependency "pry"
40
47
  spec.add_development_dependency "rake"
41
48
  spec.add_development_dependency "rspec"
42
- end
49
+ spec.add_development_dependency "yard"
50
+ end # rubocop: enable Metrics/BlockLength
@@ -19,32 +19,46 @@ 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 the extract and cleanup commands.
33
43
  def extract(session, local, remote)
34
44
  return unless local.match(/.*\.tgz/)
35
45
 
36
46
  archive_basename = File.basename(local)
37
- logger.debug("[#{LOG_PREFIX}] Extracting #{::File.join(remote, archive_basename)}")
47
+ logger.debug("[#{Express::LOG_PREFIX}] Extracting #{::File.join(remote, archive_basename)}")
38
48
  session.open_channel do |channel|
39
49
  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)}")
50
+ channel.exec("tar -xzf #{::File.join(remote, archive_basename)} -C #{remote} && rm -f #{File.join(remote, archive_basename)}")
42
51
  end
43
52
  session.loop
44
53
  end
45
54
 
46
55
  private
47
56
 
57
+ # Creats a archive of the directory provided.
58
+ #
59
+ # @param path [String] the path to the directory that will be archived.
60
+ # @param archive_path [String] the fully qualified path to the archive that will be created.
61
+ # @api private
48
62
  def create_archive(path, archive_path)
49
63
  Archive.write_open_filename(archive_path, Archive::COMPRESSION_GZIP,
50
64
  Archive::FORMAT_TAR_PAX_RESTRICTED) do |tar|
@@ -52,54 +66,66 @@ module Kitchen
52
66
  end
53
67
  end
54
68
 
69
+ # Appends the content of each item in the expanded directory path.
70
+ #
71
+ # @param tar [Archive::Writer] the instance of the archive class.
72
+ # @param path [String] the path to the directory that will be archived.
73
+ # @api private
55
74
  def write_content(tar, path)
56
75
  all_files = Dir.glob("#{path}/**/*")
57
76
  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
77
+ if File.file? f
78
+ tar.new_entry do |e|
79
+ entry(e, f, path)
80
+ tar.write_header e
81
+ tar.write_data content(f)
82
+ end
62
83
  end
63
84
  end
64
85
  end
65
86
 
87
+ # Creates the entry in the Archive for each item.
88
+ #
89
+ # @param ent [Archive::Entry] the current entry being added to the archive.
90
+ # @param file [String] the current file or directory being added to the archive.
91
+ # @param path [String] the path to the directory being archived.
92
+ # @api private
66
93
  def entry(ent, file, path)
67
- ent.pathname = path_name(file, path)
68
- ent.size = size(file) if File.file? file
94
+ ent.pathname = file.gsub(%r{#{File.dirname(path)}/}, "")
95
+ ent.size = size(file)
69
96
  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
97
+ ent.filetype = Archive::Entry::FILE
98
+ ent.atime = Time.now.to_i
99
+ ent.mtime = Time.now.to_i
85
100
  end
86
101
 
102
+ # The content of the file in binary format. Directories have no content.
103
+ #
104
+ # @param file [String] the path to the file.
105
+ # @return [String] the content of the file.
106
+ # @api private
87
107
  def content(file)
88
- File.read(file, mode: "rb") unless File.directory? file
108
+ File.read(file, mode: "rb")
89
109
  end
90
110
 
111
+ # The size of the file. Directories have no size.
112
+ #
113
+ # @param file [String] the path to the file.
114
+ # @return [Integer] the size of the file.
115
+ # @api private
91
116
  def size(file)
92
117
  content(file).size
93
118
  end
94
119
 
120
+ # The file permissions of the file.
121
+ #
122
+ # @param file [String] the path to the file or directory.
123
+ # @return [Integer] the mode of the file or directory.
124
+ # @api private
95
125
  def mode(file)
96
126
  f = File.stat(file)
97
127
  f.mode
98
128
  end
99
-
100
- def timestamp
101
- Time.now.to_i
102
- end
103
129
  end
104
130
  end
105
131
  end
@@ -17,7 +17,10 @@
17
17
  module Kitchen
18
18
  module Transport
19
19
  class Express
20
- VERSION = "1.1.0"
20
+ # The version string for Kitchen Transport Express.
21
+ #
22
+ # @author Justin Steele <justin.steele@oracle.com>
23
+ VERSION = "1.3.0"
21
24
  end
22
25
  end
23
26
  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,71 @@ 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
 
57
- def upload(locals, remote)
58
- return super unless valid_remote_requirements?
91
+ # Overrides the upload method in Kitchen::Transport::Ssh::Connection.
92
+ # The special sauce here is that we create threaded uploads of archives of the kitchen files rather than serial file uploads.
93
+ # (see Kitchen::Transport::Base::Connection#upload)
94
+ #
95
+ # @param locals [Array] the top-level list of directories and files to be transfered.
96
+ # @param remote [String] the remote directory (kitchen_root).
97
+ # @raise [ExpressFailed] if any of the threads raised an exception.
98
+ def upload(locals, remote) # rubocop: disable Metrics/MethodLength
99
+ return super unless valid_remote_requirements?(remote)
59
100
 
60
- execute("mkdir -p #{remote}")
61
101
  processed_locals = process_locals(locals)
62
- pool = Concurrent::FixedThreadPool.new([processed_locals.length, 10].min)
102
+ pool, exceptions = thread_pool(processed_locals)
63
103
  processed_locals.each do |local|
64
- pool.post { transfer(local, remote, session.options) }
104
+ pool.post do
105
+ transfer(local, remote, session.options)
106
+ rescue => e
107
+ exceptions << e.cause
108
+ end
65
109
  end
66
110
  pool.shutdown
67
111
  pool.wait_for_termination
112
+
113
+ raise ExpressFailed, exceptions.pop unless exceptions.empty?
114
+ end # rubocop: enable Metrics/MethodLength
115
+
116
+ private
117
+
118
+ # Creates the thread pool and exceptions queue.
119
+ #
120
+ # @param processed_locals [Array] list of files and archives to be uploaded.
121
+ # @return [Array(Concurrent::FixedThreadPool, Queue)]
122
+ # @api private
123
+ def thread_pool(processed_locals)
124
+ [Concurrent::FixedThreadPool.new([processed_locals.length, 10].min), Queue.new]
68
125
  end
69
126
 
70
- def valid_remote_requirements?
127
+ # Ensures the remote host has the minimum-required executables to extract the archives.
128
+ #
129
+ # @param remote [String] the remote directory (kitchen_root).
130
+ # @return [Boolean]
131
+ # @api private
132
+ def valid_remote_requirements?(remote)
71
133
  execute("(which tar && which gzip) > /dev/null")
134
+ execute("mkdir -p #{remote}")
72
135
  true
73
136
  rescue => e
74
- logger.debug("[#{LOG_PREFIX}] Requirements not met on remote host for Express transport.")
75
- logger.debug(e)
137
+ logger.debug("[#{Express::LOG_PREFIX}] Requirements not met on remote host for Express transport.")
138
+ logger.debug("[#{Express::LOG_PREFIX}] #{e}")
76
139
  false
77
140
  end
78
141
 
79
- private
80
-
142
+ # Builds an array of files we want to ship. If the top-level item is a directory, archive it and
143
+ # add the archive name to the array.
144
+ #
145
+ # @param locals [Array] the top-level list of directories and files to be transfered.
146
+ # @return [Array] the paths to the files and archives that will be transferred.
147
+ # @api private
81
148
  def process_locals(locals)
82
149
  processed_locals = []
83
150
  Array(locals).each do |local|
@@ -91,12 +158,22 @@ module Kitchen
91
158
  processed_locals
92
159
  end
93
160
 
161
+ # Uploads the archives or files to the remote host.
162
+ #
163
+ # @param local [String] a single top-level item from the upload method.
164
+ # @param remote [String] path to remote destination.
165
+ # @param opts [Hash] the ssh options that came in from the Kitchen instance.
166
+ # @raise [StandardError] if the files could not be uploaded successfully.
167
+ # @api private
94
168
  def transfer(local, remote, opts = {})
95
- logger.debug("[#{LOG_PREFIX}] Transferring #{local} to #{remote}")
169
+ logger.debug("[#{Express::LOG_PREFIX}] Transferring #{local} to #{remote}")
96
170
 
97
171
  Net::SSH.start(session.host, opts[:user], **opts) do |ssh|
98
172
  ssh.scp.upload!(local, remote, opts)
99
173
  extract(ssh, local, remote)
174
+ rescue Net::SCP::Error => ex
175
+ logger.debug("[#{Express::LOG_PREFIX}] upload failed with #{ex.message.strip}")
176
+ raise "(#{ex.message.strip})"
100
177
  end
101
178
  end
102
179
  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.3.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-14 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:
@@ -146,7 +160,11 @@ homepage: https://github.com/justintsteele/kitchen-transport-express.git
146
160
  licenses:
147
161
  - Apache-2.0
148
162
  metadata:
149
- github_repo: https://github.com/justintsteele/kitchen-transport-express
163
+ bug_tracker_uri: https://github.com/justintsteele/kitchen-transport-express/issues
164
+ changelog_uri: https://github.com/justintsteele/kitchen-transport-express/blob/main/CHANGELOG.md
165
+ documentation_uri: https://github.com/justintsteele/kitchen-transport-express/blob/main/README.md
166
+ homepage_uri: https://github.com/justintsteele/kitchen-transport-express
167
+ source_code_uri: https://github.com/justintsteele/kitchen-transport-express
150
168
  post_install_message:
151
169
  rdoc_options: []
152
170
  require_paths:
@@ -165,5 +183,5 @@ requirements: []
165
183
  rubygems_version: 3.3.7
166
184
  signing_key:
167
185
  specification_version: 4
168
- summary: Skip the long lines in transport. 15 items or less!
186
+ summary: Skip the long lines in Kitchen Transport!
169
187
  test_files: []