kitchen-rackspace 0.21.2 → 0.22.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.
@@ -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,22 @@ module Kitchen
51
74
  required_config :image_id
52
75
  required_config :public_key_path
53
76
 
77
+ # @param config [Hash] the driver configuration
54
78
  def initialize(config)
55
79
  super
56
80
  Fog.timeout = config[:wait_for].to_i
57
81
  end
58
82
 
83
+ # Creates the Cloud Server and waits until it can be logged into.
84
+ #
85
+ # Waits for the server to report ready, then optionally for RackConnect
86
+ # and Managed Service Level automation, then for the transport to accept
87
+ # a connection.
88
+ #
89
+ # @param state [Hash] mutable instance state; gains +server_id+,
90
+ # +hostname+, +username+, and +port+
91
+ # @return [void]
92
+ # @raise [Kitchen::ActionFailed] if the Rackspace API rejects the build
59
93
  def create(state)
60
94
  server = create_server
61
95
  state[:server_id] = server.id
@@ -65,11 +99,18 @@ module Kitchen
65
99
  rackconnect_check(server) if config[:rackconnect_wait]
66
100
  servicelevel_check(server) if config[:servicelevel_wait]
67
101
  state[:hostname] = hostname(server)
102
+ state[:username] = config[:username]
103
+ state[:port] = config[:port] if config[:port]
68
104
  tcp_check(state)
69
105
  rescue Fog::Errors::Error, Excon::Errors::Error => ex
70
106
  raise ActionFailed, ex.message
71
107
  end
72
108
 
109
+ # Destroys the Cloud Server, if one was created.
110
+ #
111
+ # @param state [Hash] mutable instance state; +server_id+ and +hostname+
112
+ # are removed
113
+ # @return [void]
73
114
  def destroy(state)
74
115
  return if state[:server_id].nil?
75
116
 
@@ -80,18 +121,24 @@ module Kitchen
80
121
  state.delete(:hostname)
81
122
  end
82
123
 
124
+ # Looks up the base image for the platform under test.
125
+ #
126
+ # @return [String, nil] the Rackspace image ID, or nil when the platform
127
+ # is not one the bundled image list knows about, in which case
128
+ # +image_id+ must be set explicitly
83
129
  def default_image
84
130
  images[instance.platform.name]
85
131
  end
86
132
 
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
133
+ # Generates a server name that is unique per run and fits Rackspace's
134
+ # 63-character limit.
135
+ #
136
+ # The budget is spent as base name 15, username 15, hostname 23, random
137
+ # suffix 7, and three separators, for 63 exactly. Each part is stripped
138
+ # of non-word characters and truncated to its share, so a long login or
139
+ # hostname cannot push the result over.
140
+ #
141
+ # @return [String] e.g. +default-alice-buildbox01-x7f2p9q+
95
142
  def default_name
96
143
  [
97
144
  instance.name.gsub(/\W/, "")[0..14],
@@ -101,8 +148,79 @@ module Kitchen
101
148
  ].join("-")
102
149
  end
103
150
 
151
+ # Reports what Rackspace currently thinks of the server.
152
+ #
153
+ # @param state [Hash] instance state naming the server
154
+ # @return [Hash] a Test Kitchen status hash, or the base implementation's
155
+ # answer when there is no server or Rackspace does not know it
156
+ def status(state)
157
+ return super unless state[:server_id]
158
+
159
+ server = lookup_server(state[:server_id])
160
+ return super unless server
161
+
162
+ {
163
+ live: LIVE_STATES.include?(server.state),
164
+ state: server.state,
165
+ source: "driver",
166
+ resource_id: state[:server_id],
167
+ message: "Rackspace reports the server as #{server.state}",
168
+ checked_at: Time.now.utc.iso8601,
169
+ }
170
+ end
171
+
172
+ # Checks the configuration for the mistakes that only show up as a
173
+ # confusing failure part way through +create+.
174
+ #
175
+ # @param state [Hash] mutable instance and driver state
176
+ # @return [Boolean] true when a problem was reported
177
+ def doctor(state) # rubocop:disable Lint/UnusedMethodArgument
178
+ problems = []
179
+
180
+ if config[:image_id].nil?
181
+ problems << "No image_id is set and #{instance.platform.name} is not " \
182
+ "in the bundled image list. Set image_id explicitly."
183
+ end
184
+
185
+ if config[:public_key_path].nil?
186
+ problems << "No public key was found in ~/.ssh. Set public_key_path " \
187
+ "to the key Rackspace should install on the server."
188
+ end
189
+
190
+ problems.concat(credential_problems)
191
+
192
+ problems.each { |problem| warn(problem) }
193
+ !problems.empty?
194
+ end
195
+
104
196
  private
105
197
 
198
+ # Confirms the configured credentials can actually talk to Rackspace,
199
+ # which is cheaper to learn here than half way through a converge.
200
+ #
201
+ # @return [Array<String>] a problem description, or an empty array
202
+ def credential_problems
203
+ compute.servers.summary
204
+ []
205
+ rescue Fog::Errors::Error, Excon::Errors::Error => e
206
+ ["Rackspace rejected the configured credentials for region " \
207
+ "#{config[:rackspace_region]}: #{e.message}"]
208
+ end
209
+
210
+ # Looks a server up without turning a missing one into a failure.
211
+ #
212
+ # @param server_id [String] the Rackspace server ID
213
+ # @return [Fog::Compute::RackspaceV2::Server, nil] the server, or nil
214
+ # when Rackspace does not know it or cannot be reached
215
+ def lookup_server(server_id)
216
+ compute.servers.get(server_id)
217
+ rescue Fog::Errors::Error, Excon::Errors::Error
218
+ nil
219
+ end
220
+
221
+ # Builds the fog compute connection from the configured credentials.
222
+ #
223
+ # @return [Fog::Compute] a Rackspace compute connection
106
224
  def compute
107
225
  server_def = { provider: "Rackspace" }
108
226
  opts = %i{version rackspace_username rackspace_api_key
@@ -113,6 +231,13 @@ module Kitchen
113
231
  Fog::Compute.new(server_def)
114
232
  end
115
233
 
234
+ # Bootstraps the Cloud Server from the configured options.
235
+ #
236
+ # RackConnect and Managed Service Level both need the root password left
237
+ # unlocked to do their work, so +no_passwd_lock+ is forced on whenever
238
+ # either wait is requested, regardless of how it was configured.
239
+ #
240
+ # @return [Fog::Compute::RackspaceV2::Server] the newly built server
116
241
  def create_server
117
242
  server_def = { name: config[:server_name], networks: }
118
243
  %i{image_id flavor_id public_key_path no_passwd_lock user_data config_drive}.each do |opt|
@@ -124,6 +249,9 @@ module Kitchen
124
249
  compute.servers.bootstrap(server_def)
125
250
  end
126
251
 
252
+ # The bundled platform-name to image-ID map.
253
+ #
254
+ # @return [Hash{String => String}] parsed from +data/images.json+
127
255
  def images
128
256
  @images ||= begin
129
257
  json_file = File.expand_path("../../../data/images.json", __dir__)
@@ -131,14 +259,36 @@ module Kitchen
131
259
  end
132
260
  end
133
261
 
262
+ # Waits until the server will accept a login.
263
+ #
264
+ # The TCP check does not honour +ssh_config+, which some setups need, so
265
+ # +no_ssh_tcp_check+ swaps the check for a fixed sleep of
266
+ # +no_ssh_tcp_check_sleep+ seconds instead.
267
+ #
268
+ # @param state [Hash] instance state describing how to connect
269
+ # @return [void]
134
270
  def tcp_check(state)
135
271
  # allow driver config to bypass SSH tcp check -- because
136
272
  # it doesn't respect ssh_config values that might be required
137
- wait_for_sshd(state[:hostname]) unless config[:no_ssh_tcp_check]
273
+ wait_for_sshd(state) unless config[:no_ssh_tcp_check]
138
274
  sleep(config[:no_ssh_tcp_check_sleep]) if config[:no_ssh_tcp_check]
139
275
  puts "(ssh ready)"
140
276
  end
141
277
 
278
+ # Kitchen::Driver::SSHBase used to supply this; the configured transport
279
+ # knows how to wait for the instance to accept connections.
280
+ def wait_for_sshd(state)
281
+ instance.transport.connection(state, &:wait_until_ready)
282
+ end
283
+
284
+ # Waits for RackConnect automation to finish.
285
+ #
286
+ # The server is refreshed afterwards, because RackConnect assigns a new
287
+ # public address as part of its work and the stale one would otherwise
288
+ # be handed to the transport.
289
+ #
290
+ # @param server [Fog::Compute::RackspaceV2::Server] the server to poll
291
+ # @return [void]
142
292
  def rackconnect_check(server)
143
293
  server.wait_for \
144
294
  { metadata.all["rackconnect_automation_status"] == "DEPLOYED" }
@@ -146,12 +296,21 @@ module Kitchen
146
296
  server.update # refresh accessIPv4 with new IP
147
297
  end
148
298
 
299
+ # Waits for Managed Service Level automation to finish.
300
+ #
301
+ # @param server [Fog::Compute::RackspaceV2::Server] the server to poll
302
+ # @return [void]
149
303
  def servicelevel_check(server)
150
304
  server.wait_for \
151
305
  { metadata.all["rax_service_level_automation"] == "Complete" }
152
306
  puts "(service level automation complete)"
153
307
  end
154
308
 
309
+ # Picks the address Test Kitchen should connect to.
310
+ #
311
+ # @param server [Fog::Compute::RackspaceV2::Server] the built server
312
+ # @return [String] the private address when +servicenet+ is set,
313
+ # otherwise the public one
155
314
  def hostname(server)
156
315
  if config[:servicenet] == false
157
316
  server.public_ip_address
@@ -160,6 +319,14 @@ module Kitchen
160
319
  end
161
320
  end
162
321
 
322
+ # Builds the network list for the new server.
323
+ #
324
+ # Rackspace's PublicNet and ServiceNet have fixed, well-known IDs. Any
325
+ # configured networks are added to those two rather than replacing them,
326
+ # since dropping PublicNet would leave the server unreachable.
327
+ #
328
+ # @return [Array<String>, nil] network IDs, or nil to let Rackspace
329
+ # apply its own defaults
163
330
  def networks
164
331
  base_nets = %w{
165
332
  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.0"
5
8
  end
6
9
  end
@@ -27,6 +27,121 @@ describe Kitchen::Driver::Rackspace do
27
27
  ENV["RACKSPACE_API_KEY"] = "key"
28
28
  end
29
29
 
30
+ describe "plugin metadata" do
31
+ it "declares the driver API version" do
32
+ expect(described_class.instance_variable_get(:@api_version)).to eq(2)
33
+ end
34
+
35
+ it "reports its own gem version to kitchen diagnose" do
36
+ expect(driver.diagnose_plugin[:version])
37
+ .to eq(Kitchen::Driver::RACKSPACE_VERSION)
38
+ end
39
+ end
40
+
41
+ describe "#status" do
42
+ let(:server) { double(state: "ACTIVE") }
43
+ let(:servers) { double }
44
+
45
+ before(:each) do
46
+ allow(driver).to receive(:compute).and_return(double(servers:))
47
+ end
48
+
49
+ context "with no server in state" do
50
+ it "reports an unknown status" do
51
+ expect(driver.status({})).to include(live: nil, state: "unknown")
52
+ end
53
+ end
54
+
55
+ context "with a server Rackspace does not know" do
56
+ it "reports an unknown status" do
57
+ expect(servers).to receive(:get).with("gone").and_return(nil)
58
+
59
+ expect(driver.status(server_id: "gone")).to include(state: "unknown")
60
+ end
61
+ end
62
+
63
+ context "with a live server" do
64
+ before(:each) do
65
+ allow(servers).to receive(:get).with("s-1").and_return(server)
66
+ end
67
+
68
+ it "reports the server as live" do
69
+ expect(driver.status(server_id: "s-1")).to include(
70
+ live: true, state: "ACTIVE", source: "driver", resource_id: "s-1"
71
+ )
72
+ end
73
+
74
+ it "stamps when the check happened" do
75
+ expect(driver.status(server_id: "s-1")[:checked_at])
76
+ .to match(/\A\d{4}-\d{2}-\d{2}T/)
77
+ end
78
+ end
79
+
80
+ context "with a server that is not active" do
81
+ it "reports the server as not live" do
82
+ allow(servers).to receive(:get)
83
+ .with("s-1").and_return(double(state: "ERROR"))
84
+
85
+ expect(driver.status(server_id: "s-1"))
86
+ .to include(live: false, state: "ERROR")
87
+ end
88
+ end
89
+
90
+ context "when Rackspace cannot be reached" do
91
+ it "reports an unknown status rather than raising" do
92
+ allow(servers).to receive(:get)
93
+ .and_raise(Fog::Errors::Error.new("boom"))
94
+
95
+ expect(driver.status(server_id: "s-1")).to include(state: "unknown")
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "#doctor" do
101
+ let(:config) do
102
+ { image_id: "img-1", public_key_path: "/home/user/.ssh/id_rsa.pub" }
103
+ end
104
+
105
+ before(:each) do
106
+ allow(driver).to receive(:compute)
107
+ .and_return(double(servers: double(summary: [])))
108
+ end
109
+
110
+ it "reports no problem when the configuration is complete" do
111
+ expect(driver.doctor(state)).to eq(false)
112
+ end
113
+
114
+ context "with no resolvable image" do
115
+ let(:config) { { public_key_path: "/key.pub" } }
116
+
117
+ it "reports a problem" do
118
+ allow(driver).to receive(:default_image).and_return(nil)
119
+
120
+ expect(driver.doctor(state)).to eq(true)
121
+ expect(logged_output.string).to match(/No image_id is set/)
122
+ end
123
+ end
124
+
125
+ context "with no public key" do
126
+ let(:config) { { image_id: "img-1", public_key_path: nil } }
127
+
128
+ it "reports a problem" do
129
+ expect(driver.doctor(state)).to eq(true)
130
+ expect(logged_output.string).to match(/No public key was found/)
131
+ end
132
+ end
133
+
134
+ context "with credentials Rackspace rejects" do
135
+ it "reports a problem" do
136
+ allow(driver).to receive(:compute)
137
+ .and_raise(Fog::Errors::Error.new("unauthorized"))
138
+
139
+ expect(driver.doctor(state)).to eq(true)
140
+ expect(logged_output.string).to match(/rejected the configured credentials/)
141
+ end
142
+ end
143
+ end
144
+
30
145
  describe "#initialize" do
31
146
  before(:each) do
32
147
  allow(Fog).to receive(:timeout=)
@@ -37,8 +152,8 @@ describe Kitchen::Driver::Rackspace do
37
152
  expect(driver[:version]).to eq("v2")
38
153
  end
39
154
 
40
- it "defaults to the smallest flavor size" do
41
- expect(driver[:flavor_id]).to eq("performance1-1")
155
+ it "defaults to a 2 GB general purpose flavor" do
156
+ expect(driver[:flavor_id]).to eq("general1-2")
42
157
  end
43
158
 
44
159
  it "defaults to local user's SSH public key" do
@@ -160,6 +275,12 @@ describe Kitchen::Driver::Rackspace do
160
275
  ENV["OS_REGION_NAME"] = "os_region"
161
276
  end
162
277
 
278
+ after(:each) do
279
+ ENV.delete("OS_USERNAME")
280
+ ENV.delete("OS_PASSWORD")
281
+ ENV.delete("OS_REGION_NAME")
282
+ end
283
+
163
284
  it "gets to username from $OS_USERNAME" do
164
285
  expect(driver[:rackspace_username]).to eq("os_user")
165
286
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-rackspace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.2
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Hartman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-22 00:00:00.000000000 Z
11
+ date: 2026-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fog-rackspace
@@ -33,7 +33,7 @@ dependencies:
33
33
  version: '1.35'
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: 2.6.1
36
+ version: '2.3'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,14 +43,14 @@ dependencies:
43
43
  version: '1.35'
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: 2.6.1
46
+ version: '2.3'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: test-kitchen
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '1.1'
53
+ version: '3.0'
54
54
  - - "<"
55
55
  - !ruby/object:Gem::Version
56
56
  version: '5.0'
@@ -60,7 +60,7 @@ dependencies:
60
60
  requirements:
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
- version: '1.1'
63
+ version: '3.0'
64
64
  - - "<"
65
65
  - !ruby/object:Gem::Version
66
66
  version: '5.0'
@@ -72,19 +72,22 @@ extensions: []
72
72
  extra_rdoc_files: []
73
73
  files:
74
74
  - ".github/CODEOWNERS"
75
- - ".github/dependabot.yml"
76
75
  - ".github/workflows/linters.yml"
77
76
  - ".github/workflows/publish.yaml"
78
77
  - ".gitignore"
79
78
  - ".markdownlint.yaml"
80
79
  - ".release-please-manifest.json"
81
80
  - ".rubocop.yml"
81
+ - ".yamllint"
82
+ - ".yardopts"
82
83
  - CHANGELOG.md
84
+ - CONTRIBUTING.md
83
85
  - Gemfile
84
86
  - LICENSE
85
87
  - README.md
86
88
  - Rakefile
87
89
  - data/images.json
90
+ - helpers/dump_flavor_list.rb
88
91
  - helpers/dump_image_list.rb
89
92
  - kitchen-rackspace.gemspec
90
93
  - lib/kitchen/driver/rackspace.rb
@@ -95,7 +98,7 @@ files:
95
98
  - spec/spec_helper.rb
96
99
  homepage: https://github.com/test-kitchen/kitchen-rackspace
97
100
  licenses:
98
- - Apache
101
+ - Apache-2.0
99
102
  metadata: {}
100
103
  post_install_message:
101
104
  rdoc_options: []
@@ -116,6 +119,4 @@ rubygems_version: 3.5.9
116
119
  signing_key:
117
120
  specification_version: 4
118
121
  summary: A Test Kitchen Rackspace driver built on Fog
119
- test_files:
120
- - spec/kitchen/driver/rackspace_spec.rb
121
- - spec/spec_helper.rb
122
+ test_files: []
@@ -1,8 +0,0 @@
1
- ---
2
- version: 2
3
- updates:
4
- - package-ecosystem: bundler
5
- directory: "/"
6
- schedule:
7
- interval: daily
8
- open-pull-requests-limit: 10