kitchen-rackspace 0.21.2 → 0.22.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.
@@ -1,86 +1,117 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Regenerates data/images.json from the images a Rackspace account can see.
5
+ #
6
+ # The driver maps a Test Kitchen platform name (`ubuntu-22.04`) to a Rackspace
7
+ # image ID. That mapping lives in data/images.json and has to be refreshed
8
+ # whenever Rackspace publishes new base images.
9
+ #
10
+ # Usage:
11
+ #
12
+ # export RACKSPACE_USERNAME=myuser
13
+ # export RACKSPACE_API_KEY=myapikey
14
+ # export RACKSPACE_REGION=ord # optional, defaults to dfw
15
+ #
16
+ # bundle exec ruby helpers/dump_image_list.rb # human-readable table
17
+ # bundle exec ruby helpers/dump_image_list.rb --json # data/images.json content
18
+ #
19
+ # Run it under Bundler. fog-rackspace only loads against the fog-core version
20
+ # this gem pins; on a newer fog-core, `require "fog/rackspace"` raises NameError
21
+ # on its "CDN v2" service name.
22
+ #
23
+ # Image IDs are per-region, so regenerate from the region most users build in.
24
+
1
25
  require "fog/rackspace"
2
26
  require "json" unless defined?(JSON)
3
27
 
4
- def whole?(x) # rubocop:disable Naming/UncommunicativeMethodParamName
5
- (x - x.floor) < 1e-6
6
- end
7
-
8
- i_care_about = {
9
- "Arch Linux (PVHVM)" => %w{arch arch-2016 arch-2016.8},
10
- "CentOS 7 (PVHVM)" => %w{centos centos-7},
11
- "CentOS 6 (PVHVM)" => %w{centos-6},
12
- "CoreOS (Stable)" => %w{coreos coreos-stable},
13
- "CoreOS (Beta)" => %w{coreos-beta},
14
- "CoreOS (Alpha)" => %w{coreos-alpha},
15
- "Debian 8 (Jessie) (PVHVM)" => %w{debian debian-8},
16
- "Debian 7 (Wheezy) (PVHVM)" => %w{debian-7},
17
- "Debian Testing (Stretch) (PVHVM)" => %w{debian-testing},
18
- "Debian Unstable (Sid) (PVHVM)" => %w{debian-unstable},
19
- "Fedora 25 (PVHVM)" => %w{fedora fedora-25},
20
- "Fedora 24 (PVHVM)" => %w{fedora-24},
21
- "FreeBSD 11 (PVHVM)" => %w{freebsd freebsd-11},
22
- "FreeBSD 10 (PVHVM)" => %w{freebsd-10},
23
- "Gentoo 15.3 (PVHVM)" => %w{gentoo gentoo-15 gentoo-15.3},
24
- "OpenSUSE Leap 42 (PVHVM)" => %w{opensuse opensuse-42},
25
- "Scientific Linux 7 (PVHVM)" => %w{scientific scientific-7},
26
- "Scientific Linux 6 (PVHVM)" => %w{scientific-6},
27
- "Ubuntu 16.04 LTS (Xenial Xerus) (PVHVM)" => %w{ubuntu ubuntu-16 ubuntu-16.04},
28
- "Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)" => %w{ubuntu-14 ubuntu-14.04},
29
- "Ubuntu 12.04 LTS (Precise Pangolin) (PVHVM)" => %w{ubuntu-12 ubuntu-12.04},
30
- "Vyatta Network OS 6.7R12" => %w{vyatta vyatta-6 vyatta-6.7 vyatta-6.7R12},
31
- "Windows Server 2012 R2" => %w{windows windows-2012 windows-2012R2},
32
- "Windows Server 2008 R2 SP1" => %w{windows-2008
33
- windows-2008R2
34
- windows-2008R2SP1},
35
- }
36
-
37
- names_to_clean = {
28
+ # Rackspace reports the distro in OpenStack metadata using reverse-DNS names.
29
+ # Map the ones that do not reduce to a sensible short name on their own.
30
+ DISTRO_NAMES = {
38
31
  "com.microsoft.server" => "windows",
39
- "org.fedoraproject" => "fedora",
40
32
  "org.archlinux" => "arch",
33
+ "org.fedoraproject" => "fedora",
41
34
  "org.scientificlinux" => "scientific",
42
- }
35
+ }.freeze
43
36
 
44
- compute = Fog::Compute.new(provider: "Rackspace",
45
- rackspace_username: ENV["RACKSPACE_USERNAME"],
46
- rackspace_api_key: ENV["RACKSPACE_API_KEY"],
47
- rackspace_region: "ORD")
37
+ # Builds the platform aliases for one image.
38
+ #
39
+ # An image advertising Ubuntu 22.04 yields "ubuntu-22.04", plus "ubuntu-22" as
40
+ # a convenience. The bare distro name ("ubuntu") is assigned separately, to the
41
+ # newest version found, so that it does not depend on iteration order.
42
+ #
43
+ # @param image [Fog::Compute::RackspaceV2::Image] the image to describe
44
+ # @return [Array(String, String, Array<String>), nil] distro, version, and
45
+ # aliases, or nil when the image carries no usable OpenStack metadata
46
+ def describe(image)
47
+ meta = image.metadata
48
+ distro_id = meta["org.openstack__1__os_distro"]
49
+ version = meta["org.openstack__1__os_version"]
50
+ return nil if distro_id.nil? || version.nil?
48
51
 
49
- aliases = i_care_about.values.flatten
50
- res = aliases.each_with_object({}) do |a, hsh|
51
- raise "Alias '#{a}' was listed twice" if hsh.include?(a)
52
+ distro = DISTRO_NAMES.fetch(distro_id) { distro_id.split(".").last }
52
53
 
53
- hsh[a] = nil
54
- hsh
54
+ aliases = ["#{distro}-#{version}"]
55
+ # "ubuntu-22" alongside "ubuntu-22.04", but not "centos-7" twice.
56
+ major = version.split(".").first
57
+ aliases << "#{distro}-#{major}" if major != version
58
+
59
+ [distro, version, aliases]
60
+ end
61
+
62
+ # Sorts version strings numerically, so that 22.04 beats 8 and 9 beats 10 is
63
+ # never asserted. Non-numeric segments (Vyatta's "6.7R12") sort last.
64
+ #
65
+ # @param version [String] an OS version
66
+ # @return [Array<Integer>] a comparable key
67
+ def version_key(version)
68
+ version.split(".").map(&:to_i)
55
69
  end
56
70
 
57
- compute.images.select { |i| i_care_about.key?(i.name) }.each do |img|
58
- image_metadata = img.metadata
71
+ compute = Fog::Compute.new(
72
+ provider: "Rackspace",
73
+ rackspace_username: ENV.fetch("RACKSPACE_USERNAME"),
74
+ rackspace_api_key: ENV.fetch("RACKSPACE_API_KEY"),
75
+ rackspace_region: ENV.fetch("RACKSPACE_REGION", "dfw"),
76
+ version: :v2
77
+ )
59
78
 
60
- if image_metadata["org.openstack__1__os_distro"] &&
61
- image_metadata["org.openstack__1__os_version"]
79
+ images = compute.images.to_a
80
+ abort "No images visible to this account." if images.empty?
62
81
 
63
- distro_id = image_metadata["org.openstack__1__os_distro"]
64
- version = image_metadata["org.openstack__1__os_version"]
82
+ if ARGV.include?("--json")
83
+ mapping = {}
84
+ newest = {}
65
85
 
66
- distro = if names_to_clean.include?(distro_id)
67
- names_to_clean[distro_id]
68
- else
69
- distro_id.split(".").last
70
- end
86
+ images.each do |image|
87
+ described = describe(image)
88
+ next if described.nil?
71
89
 
72
- res["#{distro}-#{version}"] = img.id
90
+ distro, version, aliases = described
91
+ aliases.each { |a| mapping[a] = image.id }
73
92
 
74
- # if it's a whole number non-zero version, also add
75
- # a dot-zero (centos-7 vs centos-7.0)
76
- res["#{distro}-#{version}.0"] = img.id if version != "0" && version =~ /^\s*\d+\s*$/
93
+ current = newest[distro]
94
+ if current.nil? || (version_key(version) <=> version_key(current[:version])) == 1
95
+ newest[distro] = { version:, id: image.id }
96
+ end
77
97
  end
78
98
 
79
- i_care_about[img.name].each { |a| res[a] = img.id }
80
- i_care_about.delete(img.name)
81
- end
99
+ # The bare distro name points at the newest version of that distro.
100
+ newest.each { |distro, info| mapping[distro] = info[:id] }
101
+
102
+ puts JSON.pretty_generate(mapping.sort.to_h)
103
+ else
104
+ rows = images.map do |image|
105
+ described = describe(image)
106
+ [image.id, image.name, described ? described[2].join(", ") : "-"]
107
+ end.sort_by { |row| row[1] }
82
108
 
83
- raise "Couldn't find some images we expected: #{i_care_about.keys}" unless i_care_about.empty?
109
+ widths = [0, 1, 2].map { |i| rows.map { |r| r[i].length }.max }
110
+ header = ["IMAGE ID", "NAME", "PLATFORM NAMES"]
111
+ widths = widths.each_with_index.map { |w, i| [w, header[i].length].max }
84
112
 
85
- # sort these to make them pretty
86
- puts JSON.pretty_generate(res.sort.to_h)
113
+ puts header.each_with_index.map { |h, i| h.ljust(widths[i]) }.join(" ")
114
+ puts widths.map { |w| "-" * w }.join(" ")
115
+ rows.each { |row| puts row.each_with_index.map { |c, i| c.ljust(widths[i]) }.join(" ") }
116
+ warn "\n#{rows.count} images. Re-run with --json to regenerate data/images.json."
117
+ end
@@ -10,16 +10,18 @@ Gem::Specification.new do |spec|
10
10
  spec.description = "A Test Kitchen Rackspace driver"
11
11
  spec.summary = "A Test Kitchen Rackspace driver built on Fog"
12
12
  spec.homepage = "https://github.com/test-kitchen/kitchen-rackspace"
13
- spec.license = "Apache"
13
+ spec.license = "Apache-2.0"
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
17
  spec.require_paths = %w{lib}
19
18
 
20
19
  spec.required_ruby_version = ">= 3.1"
21
20
 
22
21
  spec.add_dependency "fog-rackspace", "~> 0.1.6"
23
- spec.add_dependency "fog-core", ">= 1.35", "< 2.6.1"
24
- spec.add_dependency "test-kitchen", ">= 1.1", "< 5.0"
22
+ # fog-rackspace 0.1.6 registers a service literally named "CDN v2"; fog-core
23
+ # 2.3.0+ passes that through const_defined?, which rejects it as an invalid
24
+ # constant name, so `require "fog/rackspace"` raises NameError.
25
+ spec.add_dependency "fog-core", ">= 1.35", "< 2.3"
26
+ spec.add_dependency "test-kitchen", ">= 3.0", "< 5.0"
25
27
  end
@@ -4,12 +4,35 @@ require "kitchen"
4
4
  require "etc" unless defined?(Etc)
5
5
  require "socket" unless defined?(Socket)
6
6
  require "json" unless defined?(JSON)
7
+ require "time" unless defined?(Time.now.iso8601)
8
+ require_relative "rackspace_version"
7
9
 
8
10
  module Kitchen
11
+ # Test Kitchen's driver plugins.
9
12
  module Driver
10
- class Rackspace < Kitchen::Driver::SSHBase
13
+ # Test Kitchen driver for Rackspace Cloud Servers.
14
+ #
15
+ # Creates a Cloud Server for the instance under test, waits for it to
16
+ # become reachable, and destroys it afterwards. Talking to the server is
17
+ # the transport's job; this driver only tells Test Kitchen where it is and
18
+ # who to log in as.
19
+ #
20
+ # Rackspace can run post-build automation -- RackConnect and Managed
21
+ # Service Level -- that keeps modifying a server after it first reports
22
+ # +ready+, including changing its public address. Set +rackconnect_wait+
23
+ # or +servicelevel_wait+ so the driver waits for that to finish before
24
+ # handing the server over, or the transport will connect to an address
25
+ # that is about to change.
26
+ class Rackspace < Kitchen::Driver::Base
27
+ kitchen_driver_api_version 2
28
+
29
+ plugin_version Kitchen::Driver::RACKSPACE_VERSION
30
+
31
+ # Server states Rackspace reports for a server that is up and reachable.
32
+ LIVE_STATES = %w{ACTIVE}.freeze
33
+
11
34
  default_config :version, "v2"
12
- default_config :flavor_id, "performance1-1"
35
+ default_config :flavor_id, "general1-2"
13
36
  default_config :username, "root"
14
37
  default_config :port, "22"
15
38
  default_config :wait_for, 600
@@ -51,11 +74,27 @@ module Kitchen
51
74
  required_config :image_id
52
75
  required_config :public_key_path
53
76
 
77
+ # Sets up the driver and applies +wait_for+ as fog's global timeout.
78
+ #
79
+ # +Fog.timeout+ is process-wide, so the last driver constructed wins if
80
+ # several are in play.
81
+ #
82
+ # @param config [Hash] the driver configuration
54
83
  def initialize(config)
55
84
  super
56
85
  Fog.timeout = config[:wait_for].to_i
57
86
  end
58
87
 
88
+ # Creates the Cloud Server and waits until it can be logged into.
89
+ #
90
+ # Waits for the server to report ready, then optionally for RackConnect
91
+ # and Managed Service Level automation, then for the transport to accept
92
+ # a connection.
93
+ #
94
+ # @param state [Hash] mutable instance state; gains +server_id+,
95
+ # +hostname+, +username+, and +port+
96
+ # @return [void]
97
+ # @raise [Kitchen::ActionFailed] if the Rackspace API rejects the build
59
98
  def create(state)
60
99
  server = create_server
61
100
  state[:server_id] = server.id
@@ -65,11 +104,18 @@ module Kitchen
65
104
  rackconnect_check(server) if config[:rackconnect_wait]
66
105
  servicelevel_check(server) if config[:servicelevel_wait]
67
106
  state[:hostname] = hostname(server)
107
+ state[:username] = config[:username]
108
+ state[:port] = config[:port] if config[:port]
68
109
  tcp_check(state)
69
110
  rescue Fog::Errors::Error, Excon::Errors::Error => ex
70
111
  raise ActionFailed, ex.message
71
112
  end
72
113
 
114
+ # Destroys the Cloud Server, if one was created.
115
+ #
116
+ # @param state [Hash] mutable instance state; +server_id+ and +hostname+
117
+ # are removed
118
+ # @return [void]
73
119
  def destroy(state)
74
120
  return if state[:server_id].nil?
75
121
 
@@ -80,18 +126,24 @@ module Kitchen
80
126
  state.delete(:hostname)
81
127
  end
82
128
 
129
+ # Looks up the base image for the platform under test.
130
+ #
131
+ # @return [String, nil] the Rackspace image ID, or nil when the platform
132
+ # is not one the bundled image list knows about, in which case
133
+ # +image_id+ must be set explicitly
83
134
  def default_image
84
135
  images[instance.platform.name]
85
136
  end
86
137
 
87
- # Generate what should be a unique server name up to 63 total chars
88
- # Base name: 15
89
- # Username: 15
90
- # Hostname: 23
91
- # Random string: 7
92
- # Separators: 3
93
- # ================
94
- # Total: 63
138
+ # Generates a server name that is unique per run and fits Rackspace's
139
+ # 63-character limit.
140
+ #
141
+ # The budget is spent as base name 15, username 15, hostname 23, random
142
+ # suffix 7, and three separators, for 63 exactly. Each part is stripped
143
+ # of non-word characters and truncated to its share, so a long login or
144
+ # hostname cannot push the result over.
145
+ #
146
+ # @return [String] e.g. +default-alice-buildbox01-x7f2p9q+
95
147
  def default_name
96
148
  [
97
149
  instance.name.gsub(/\W/, "")[0..14],
@@ -101,8 +153,85 @@ module Kitchen
101
153
  ].join("-")
102
154
  end
103
155
 
156
+ # Reports what Rackspace currently thinks of the server.
157
+ #
158
+ # @param state [Hash] instance state naming the server
159
+ # @return [Hash] a Test Kitchen status hash, or the base implementation's
160
+ # answer when there is no server or Rackspace does not know it
161
+ def status(state)
162
+ return super unless state[:server_id]
163
+
164
+ server = lookup_server(state[:server_id])
165
+ return super unless server
166
+
167
+ {
168
+ live: LIVE_STATES.include?(server.state),
169
+ state: server.state,
170
+ source: "driver",
171
+ resource_id: state[:server_id],
172
+ message: "Rackspace reports the server as #{server.state}",
173
+ checked_at: Time.now.utc.iso8601,
174
+ }
175
+ end
176
+
177
+ # Checks the configuration for the mistakes that only show up as a
178
+ # confusing failure part way through +create+.
179
+ #
180
+ # @param state [Hash] instance state; accepted for the Test Kitchen hook
181
+ # signature and not read
182
+ # @return [Boolean] true when a problem was reported
183
+ def doctor(state) # rubocop:disable Lint/UnusedMethodArgument
184
+ problems = []
185
+
186
+ if config[:image_id].nil?
187
+ problems << "No image_id is set and #{instance.platform.name} is not " \
188
+ "in the bundled image list. Set image_id explicitly."
189
+ end
190
+
191
+ if config[:public_key_path].nil?
192
+ problems << "No public key was found in ~/.ssh. Set public_key_path " \
193
+ "to the key Rackspace should install on the server."
194
+ end
195
+
196
+ problems.concat(credential_problems)
197
+
198
+ problems.each { |problem| warn(problem) }
199
+ !problems.empty?
200
+ end
201
+
104
202
  private
105
203
 
204
+ # Confirms the configured credentials can actually talk to Rackspace,
205
+ # which is cheaper to learn here than half way through a converge.
206
+ #
207
+ # Listing servers is the cheapest authenticated call fog-rackspace
208
+ # offers: +GET /servers+ returns only an ID and a name per server, and
209
+ # it exercises the compute endpoint for the configured region rather
210
+ # than just the identity endpoint.
211
+ #
212
+ # @return [Array<String>] a problem description, or an empty array
213
+ def credential_problems
214
+ compute.servers.all
215
+ []
216
+ rescue Fog::Errors::Error, Excon::Errors::Error => e
217
+ ["Rackspace rejected the configured credentials for region " \
218
+ "#{config[:rackspace_region]}: #{e.message}"]
219
+ end
220
+
221
+ # Looks a server up without turning a missing one into a failure.
222
+ #
223
+ # @param server_id [String] the Rackspace server ID
224
+ # @return [Fog::Compute::RackspaceV2::Server, nil] the server, or nil
225
+ # when Rackspace does not know it or cannot be reached
226
+ def lookup_server(server_id)
227
+ compute.servers.get(server_id)
228
+ rescue Fog::Errors::Error, Excon::Errors::Error
229
+ nil
230
+ end
231
+
232
+ # Builds the fog compute connection from the configured credentials.
233
+ #
234
+ # @return [Fog::Compute] a Rackspace compute connection
106
235
  def compute
107
236
  server_def = { provider: "Rackspace" }
108
237
  opts = %i{version rackspace_username rackspace_api_key
@@ -113,17 +242,33 @@ module Kitchen
113
242
  Fog::Compute.new(server_def)
114
243
  end
115
244
 
245
+ # Bootstraps the Cloud Server from the configured options.
246
+ #
247
+ # RackConnect and Managed Service Level both need the root password left
248
+ # unlocked to do their work, so +no_passwd_lock+ is forced on whenever
249
+ # either wait is requested, regardless of how it was configured.
250
+ #
251
+ # @note fog's +bootstrap+ calls +Server#setup+, which rescues
252
+ # +Errno::ECONNREFUSED+ and retries forever, one second apart. A server
253
+ # that never opens port 22 hangs here rather than failing, and
254
+ # +wait_for+ does not bound it.
255
+ #
256
+ # @return [Fog::Compute::RackspaceV2::Server] the newly built server
116
257
  def create_server
117
258
  server_def = { name: config[:server_name], networks: }
118
259
  %i{image_id flavor_id public_key_path no_passwd_lock user_data config_drive}.each do |opt|
119
260
  server_def[opt] = config[opt]
120
261
  end
121
- # see @note on bootstrap def about rackconnect
262
+ # RackConnect and Managed Service Level need the root password left
263
+ # unlocked; see the note above.
122
264
  no_passwd_lock = config[:rackconnect_wait] || config[:servicelevel_wait]
123
265
  server_def[:no_passwd_lock] = no_passwd_lock if no_passwd_lock
124
266
  compute.servers.bootstrap(server_def)
125
267
  end
126
268
 
269
+ # The bundled platform-name to image-ID map.
270
+ #
271
+ # @return [Hash{String => String}] parsed from +data/images.json+
127
272
  def images
128
273
  @images ||= begin
129
274
  json_file = File.expand_path("../../../data/images.json", __dir__)
@@ -131,14 +276,41 @@ module Kitchen
131
276
  end
132
277
  end
133
278
 
279
+ # Waits until the server will accept a login.
280
+ #
281
+ # The TCP check does not honour +ssh_config+, which some setups need, so
282
+ # +no_ssh_tcp_check+ swaps the check for a fixed sleep of
283
+ # +no_ssh_tcp_check_sleep+ seconds instead.
284
+ #
285
+ # @param state [Hash] instance state describing how to connect
286
+ # @return [void]
134
287
  def tcp_check(state)
135
288
  # allow driver config to bypass SSH tcp check -- because
136
289
  # it doesn't respect ssh_config values that might be required
137
- wait_for_sshd(state[:hostname]) unless config[:no_ssh_tcp_check]
290
+ wait_for_sshd(state) unless config[:no_ssh_tcp_check]
138
291
  sleep(config[:no_ssh_tcp_check_sleep]) if config[:no_ssh_tcp_check]
139
292
  puts "(ssh ready)"
140
293
  end
141
294
 
295
+ # Blocks until the configured transport can connect to the instance.
296
+ #
297
+ # Kitchen::Driver::SSHBase used to supply this; the configured transport
298
+ # knows how to wait for the instance to accept connections.
299
+ #
300
+ # @param state [Hash] instance state describing how to connect
301
+ # @return [void]
302
+ def wait_for_sshd(state)
303
+ instance.transport.connection(state, &:wait_until_ready)
304
+ end
305
+
306
+ # Waits for RackConnect automation to finish.
307
+ #
308
+ # The server is refreshed afterwards, because RackConnect assigns a new
309
+ # public address as part of its work and the stale one would otherwise
310
+ # be handed to the transport.
311
+ #
312
+ # @param server [Fog::Compute::RackspaceV2::Server] the server to poll
313
+ # @return [void]
142
314
  def rackconnect_check(server)
143
315
  server.wait_for \
144
316
  { metadata.all["rackconnect_automation_status"] == "DEPLOYED" }
@@ -146,12 +318,21 @@ module Kitchen
146
318
  server.update # refresh accessIPv4 with new IP
147
319
  end
148
320
 
321
+ # Waits for Managed Service Level automation to finish.
322
+ #
323
+ # @param server [Fog::Compute::RackspaceV2::Server] the server to poll
324
+ # @return [void]
149
325
  def servicelevel_check(server)
150
326
  server.wait_for \
151
327
  { metadata.all["rax_service_level_automation"] == "Complete" }
152
328
  puts "(service level automation complete)"
153
329
  end
154
330
 
331
+ # Picks the address Test Kitchen should connect to.
332
+ #
333
+ # @param server [Fog::Compute::RackspaceV2::Server] the built server
334
+ # @return [String] the private address when +servicenet+ is set,
335
+ # otherwise the public one
155
336
  def hostname(server)
156
337
  if config[:servicenet] == false
157
338
  server.public_ip_address
@@ -160,6 +341,14 @@ module Kitchen
160
341
  end
161
342
  end
162
343
 
344
+ # Builds the network list for the new server.
345
+ #
346
+ # Rackspace's PublicNet and ServiceNet have fixed, well-known IDs. Any
347
+ # configured networks are added to those two rather than replacing them,
348
+ # since dropping PublicNet would leave the server unreachable.
349
+ #
350
+ # @return [Array<String>, nil] network IDs, or nil to let Rackspace
351
+ # apply its own defaults
163
352
  def networks
164
353
  base_nets = %w{
165
354
  00000000-0000-0000-0000-000000000000
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
+ # Test Kitchen's top-level namespace.
2
3
  module Kitchen
4
+ # Test Kitchen's driver plugins.
3
5
  module Driver
4
- RACKSPACE_VERSION = "0.21.2"
6
+ # The version of the kitchen-rackspace gem.
7
+ RACKSPACE_VERSION = "0.22.1"
5
8
  end
6
9
  end