beaker-google 1.2.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 +4 -4
- data/.github/release.yml +41 -0
- data/.github/workflows/ci.yml +11 -8
- data/.github/workflows/release.yml +91 -17
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +20 -0
- data/CHANGELOG.md +32 -0
- data/Gemfile +8 -0
- data/README.md +2 -2
- data/Rakefile +7 -0
- data/beaker-google.gemspec +9 -6
- data/lib/beaker/hypervisor/google_compute.rb +4 -4
- data/lib/beaker/hypervisor/google_compute_helper.rb +15 -5
- data/lib/beaker-google/version.rb +1 -1
- data/spec/classes/beaker/google_compute_constants_and_defaults_spec.rb +141 -0
- data/spec/classes/beaker/google_compute_helper_api_version_spec.rb +37 -0
- data/spec/classes/beaker/google_compute_helper_configuration_validation_spec.rb +100 -0
- data/spec/classes/beaker/google_compute_helper_constants_and_defaults_spec.rb +66 -0
- data/spec/classes/beaker/google_compute_helper_edge_cases_spec.rb +173 -0
- data/spec/classes/beaker/google_compute_helper_error_handling_spec.rb +105 -0
- data/spec/classes/beaker/google_compute_helper_spec.rb +368 -0
- data/spec/classes/beaker/google_compute_helper_utilities_spec.rb +95 -0
- data/spec/classes/beaker/google_compute_initialization_spec.rb +52 -0
- data/spec/classes/beaker/google_compute_provision_spec.rb +68 -0
- data/spec/classes/beaker/google_compute_spec.rb +95 -0
- data/spec/classes/beaker/google_compute_ssh_key_management_spec.rb +115 -0
- data/spec/classes/beaker/google_spec.rb +68 -0
- data/spec/classes/beaker-google/beaker_google_version_spec.rb +19 -0
- data/spec/classes/beaker_integration_spec.rb +67 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/google_compute_shared_setup.rb +138 -0
- data/spec/support/shared_mocks.rb +89 -0
- metadata +59 -15
- data/.github/workflows/codeql-analysis.yml +0 -70
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Add mutex_m requirement for Ruby 3.4.0 compatibility
|
|
4
|
+
begin
|
|
5
|
+
require 'mutex_m'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
# mutex_m is not available on all Ruby versions
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Require core dependencies
|
|
11
|
+
require 'rspec'
|
|
12
|
+
require 'rspec/its'
|
|
13
|
+
require 'fakefs/spec_helpers'
|
|
14
|
+
|
|
15
|
+
# Require beaker first to establish the base classes
|
|
16
|
+
require 'beaker'
|
|
17
|
+
|
|
18
|
+
# Require the actual implementation before mocks to avoid conflicts
|
|
19
|
+
require 'beaker/hypervisor/google_compute_helper'
|
|
20
|
+
|
|
21
|
+
# Load shared mock classes
|
|
22
|
+
require File.join(File.dirname(__FILE__), '../../support/shared_mocks')
|
|
23
|
+
|
|
24
|
+
RSpec.describe Beaker::GoogleComputeHelper do
|
|
25
|
+
subject(:google_compute_helper) { described_class.new(options) }
|
|
26
|
+
|
|
27
|
+
let(:options) do
|
|
28
|
+
{
|
|
29
|
+
gce_project: 'test-project',
|
|
30
|
+
gce_zone: 'us-central1-a',
|
|
31
|
+
gce_network: 'default',
|
|
32
|
+
gce_ports: ['80/tcp', '443/tcp'],
|
|
33
|
+
logger: logger,
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
let(:logger) { instance_double(Beaker::Logger, debug: nil, info: nil, warn: nil, error: nil) }
|
|
38
|
+
|
|
39
|
+
# Stub Google API classes to avoid actual authentication
|
|
40
|
+
before do
|
|
41
|
+
client_options = instance_double(Google::Apis::ClientOptions)
|
|
42
|
+
allow(client_options).to receive(:application_name=)
|
|
43
|
+
allow(client_options).to receive(:application_version=)
|
|
44
|
+
allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
|
|
45
|
+
|
|
46
|
+
allow(Google::Auth::ServiceAccountCredentials).to receive(:from_env).and_return(
|
|
47
|
+
instance_double(Google::Auth::ServiceAccountCredentials),
|
|
48
|
+
)
|
|
49
|
+
allow(Google::Auth).to receive(:get_application_default).and_return(
|
|
50
|
+
instance_double(Google::Auth::ServiceAccountCredentials),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
compute_service = instance_double(Google::Apis::ComputeV1::ComputeService)
|
|
54
|
+
allow(compute_service).to receive(:authorization=)
|
|
55
|
+
allow(compute_service).to receive(:request_options).and_return(Google::Apis::RequestOptions.new)
|
|
56
|
+
allow(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
|
|
57
|
+
|
|
58
|
+
oslogin_service = instance_double(Google::Apis::OsloginV1::CloudOSLoginService)
|
|
59
|
+
allow(oslogin_service).to receive(:authorization=)
|
|
60
|
+
allow(Google::Apis::OsloginV1::CloudOSLoginService).to receive(:new).and_return(oslogin_service)
|
|
61
|
+
|
|
62
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
63
|
+
allow(ENV).to receive(:fetch).and_call_original
|
|
64
|
+
allow(ENV).to receive(:[]).with('GOOGLE_CLOUD_UNIVERSE_DOMAIN').and_return(nil)
|
|
65
|
+
allow(ENV).to receive(:[]).with('GOOGLE_CLOUD_PROJECT').and_return(nil)
|
|
66
|
+
allow(ENV).to receive(:[]).with('GCLOUD_PROJECT').and_return(nil)
|
|
67
|
+
allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return(nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#initialize' do
|
|
71
|
+
context 'with environment variables' do
|
|
72
|
+
before do
|
|
73
|
+
allow(ENV).to receive(:[]).with('BEAKER_gce_project').and_return('env-project')
|
|
74
|
+
allow(ENV).to receive(:fetch).with('BEAKER_gce_project', nil).and_return('env-project')
|
|
75
|
+
allow(ENV).to receive(:fetch).with('BEAKER_gce_zone', 'us-central1-a').and_return('us-west1-a')
|
|
76
|
+
allow(ENV).to receive(:fetch).with('BEAKER_gce_network', 'default').and_return('custom-network')
|
|
77
|
+
allow(ENV).to receive(:fetch).with('BEAKER_gce_subnetwork', nil).and_return('custom-subnet')
|
|
78
|
+
allow(ENV).to receive(:fetch).with('BEAKER_gce_ports', '').and_return('22/tcp,80/tcp')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'uses environment variables when available' do
|
|
82
|
+
helper = described_class.new(options.merge(gce_project: nil))
|
|
83
|
+
expect(helper.instance_variable_get(:@options)[:gce_project]).to eq('env-project')
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context 'without required project' do
|
|
88
|
+
before do
|
|
89
|
+
allow(ENV).to receive(:[]).with('BEAKER_gce_project').and_return(nil)
|
|
90
|
+
allow(ENV).to receive(:fetch).with('BEAKER_gce_project', nil).and_return(nil)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'raises an error when gce_project is not provided' do
|
|
94
|
+
expect do
|
|
95
|
+
described_class.new(options.merge(gce_project: nil))
|
|
96
|
+
end.to raise_error('You must specify a gce_project for Google Compute Engine instances!')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context 'with invalid ports' do
|
|
101
|
+
it 'raises an error for invalid port format' do
|
|
102
|
+
expect do
|
|
103
|
+
described_class.new(options.merge(gce_ports: ['invalid-port']))
|
|
104
|
+
end.to raise_error(/Invalid format for port/)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'raises an error for invalid protocol' do
|
|
108
|
+
expect do
|
|
109
|
+
described_class.new(options.merge(gce_ports: ['80/invalid']))
|
|
110
|
+
end.to raise_error(/Invalid value 'invalid' for protocol/)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe '#client' do
|
|
116
|
+
it 'sets the application name on Google API client options' do
|
|
117
|
+
client_options = instance_spy(Google::Apis::ClientOptions)
|
|
118
|
+
allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
|
|
119
|
+
|
|
120
|
+
google_compute_helper.send(:client, Beaker::Version::STRING)
|
|
121
|
+
|
|
122
|
+
expect(client_options).to have_received(:application_name=).with('beaker-google').at_least(:once)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'sets the application version on Google API client options' do
|
|
126
|
+
client_options = instance_spy(Google::Apis::ClientOptions)
|
|
127
|
+
allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
|
|
128
|
+
|
|
129
|
+
google_compute_helper.send(:client, Beaker::Version::STRING)
|
|
130
|
+
|
|
131
|
+
expect(client_options).to have_received(:application_version=).with(Beaker::Version::STRING).at_least(:once)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '#authenticate' do
|
|
136
|
+
context 'when GOOGLE_APPLICATION_CREDENTIALS environment variable is set' do
|
|
137
|
+
let(:auth_double) { instance_double(Google::Auth::ServiceAccountCredentials) }
|
|
138
|
+
|
|
139
|
+
before do
|
|
140
|
+
allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return('/path/to/credentials.json')
|
|
141
|
+
allow(Google::Auth::ServiceAccountCredentials).to receive(:from_env)
|
|
142
|
+
.with(scope: described_class::GCP_AUTH_SCOPE)
|
|
143
|
+
.and_return(auth_double)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'uses service account credentials from environment' do
|
|
147
|
+
# Create a fresh instance to test authentication
|
|
148
|
+
fresh_instance = described_class.allocate
|
|
149
|
+
result = fresh_instance.send(:authenticate)
|
|
150
|
+
expect(result).to eq(auth_double)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
context 'when GOOGLE_APPLICATION_CREDENTIALS is not set' do
|
|
155
|
+
let(:auth_double) { instance_double(Google::Auth::ServiceAccountCredentials) }
|
|
156
|
+
|
|
157
|
+
before do
|
|
158
|
+
allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return(nil)
|
|
159
|
+
allow(Google::Auth).to receive(:get_application_default)
|
|
160
|
+
.with(described_class::GCP_AUTH_SCOPE)
|
|
161
|
+
.and_return(auth_double)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'falls back to application default credentials' do
|
|
165
|
+
# Create a fresh instance to test authentication
|
|
166
|
+
fresh_instance = described_class.allocate
|
|
167
|
+
result = fresh_instance.send(:authenticate)
|
|
168
|
+
expect(result).to eq(auth_double)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe '#default_zone' do
|
|
174
|
+
it 'returns the configured zone from options' do
|
|
175
|
+
expect(google_compute_helper.default_zone).to eq(options[:gce_zone])
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
describe '#default_region' do
|
|
180
|
+
it 'extracts region from zone in options' do
|
|
181
|
+
expected_region = options[:gce_zone].split('-')[0..1].join('-')
|
|
182
|
+
expect(google_compute_helper.default_region).to eq(expected_region)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe '#default_network' do
|
|
187
|
+
it 'returns the configured network from options' do
|
|
188
|
+
expect(google_compute_helper.default_network).to eq(options[:gce_network])
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
describe 'API methods' do
|
|
193
|
+
let(:compute_service) { instance_double(Google::Apis::ComputeV1::ComputeService) }
|
|
194
|
+
|
|
195
|
+
before do
|
|
196
|
+
google_compute_helper.instance_variable_set(:@compute, compute_service)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe '#get_network' do
|
|
200
|
+
it 'retrieves network information' do
|
|
201
|
+
allow(compute_service).to receive(:get_network).and_return(MockNetwork.new)
|
|
202
|
+
expect(google_compute_helper.get_network).to be_a(MockNetwork)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe '#get_machine_type' do
|
|
207
|
+
it 'retrieves machine type information' do
|
|
208
|
+
allow(compute_service).to receive(:get_machine_type).and_return(MockMachineType.new)
|
|
209
|
+
expect(google_compute_helper.get_machine_type('n1-standard-1')).to be_a(MockMachineType)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
describe '#get_image' do
|
|
214
|
+
it 'retrieves image information' do
|
|
215
|
+
allow(compute_service).to receive(:get_image).and_return(MockImage.new)
|
|
216
|
+
expect(google_compute_helper.get_image('test-project', 'test-image')).to be_a(MockImage)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
describe '#get_latest_image_from_family' do
|
|
221
|
+
before do
|
|
222
|
+
allow(compute_service).to receive(:get_image_from_family).and_return(MockImage.new)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'retrieves latest image from family' do
|
|
226
|
+
expect(google_compute_helper.get_latest_image_from_family('test-project', 'test-family')).to be_a(MockImage)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe '#create_firewall' do
|
|
231
|
+
let(:network) { instance_double(Google::Apis::ComputeV1::Network, self_link: 'network-link') }
|
|
232
|
+
let(:result) do
|
|
233
|
+
google_compute_helper.create_firewall(
|
|
234
|
+
'test-firewall',
|
|
235
|
+
network,
|
|
236
|
+
allow: ['80/tcp'],
|
|
237
|
+
source_ranges: ['0.0.0.0/0'],
|
|
238
|
+
target_tags: ['test-tag'],
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
before do
|
|
243
|
+
allow(compute_service).to receive_messages(insert_firewall: MockOperation.new,
|
|
244
|
+
wait_global_operation: MockOperation.new)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'creates a firewall rule' do
|
|
248
|
+
expect(result).to be_a(MockOperation)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
describe '#delete_firewall' do
|
|
253
|
+
let(:result) { google_compute_helper.delete_firewall('test-firewall') }
|
|
254
|
+
|
|
255
|
+
before do
|
|
256
|
+
allow(compute_service).to receive_messages(delete_firewall: MockOperation.new,
|
|
257
|
+
wait_global_operation: MockOperation.new)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'deletes a firewall rule' do
|
|
261
|
+
expect(result).to be_a(MockOperation)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
describe '#create_instance' do
|
|
266
|
+
let(:image) { instance_double(Google::Apis::ComputeV1::Image, self_link: 'image-link') }
|
|
267
|
+
let(:machine_type) { instance_double(Google::Apis::ComputeV1::MachineType, self_link: 'machine-link') }
|
|
268
|
+
|
|
269
|
+
before do
|
|
270
|
+
allow(compute_service).to receive_messages(
|
|
271
|
+
get_network: MockNetwork.new,
|
|
272
|
+
get_subnetwork: MockSubnetwork.new,
|
|
273
|
+
insert_instance: MockOperation.new,
|
|
274
|
+
wait_zone_operation: MockOperation.new,
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it 'creates an instance' do
|
|
279
|
+
result = google_compute_helper.create_instance('test-instance', image, machine_type, 20, 'test-host')
|
|
280
|
+
expect(result).to be_a(MockOperation)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
describe '#delete_instance' do
|
|
285
|
+
it 'deletes an instance' do
|
|
286
|
+
allow(compute_service).to receive_messages(delete_instance: MockOperation.new,
|
|
287
|
+
wait_zone_operation: MockOperation.new)
|
|
288
|
+
result = google_compute_helper.delete_instance('test-instance')
|
|
289
|
+
expect(result).to be_a(MockOperation)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
describe '#get_instance' do
|
|
294
|
+
it 'retrieves instance information' do
|
|
295
|
+
allow(compute_service).to receive(:get_instance).and_return(MockInstance.new)
|
|
296
|
+
result = google_compute_helper.get_instance('test-instance')
|
|
297
|
+
expect(result).to be_a(MockInstance)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
describe '#add_instance_tag' do
|
|
302
|
+
before do
|
|
303
|
+
allow(compute_service).to receive_messages(
|
|
304
|
+
get_instance: MockInstance.new,
|
|
305
|
+
set_instance_tags: MockOperation.new,
|
|
306
|
+
wait_zone_operation: MockOperation.new,
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it 'adds a tag to an instance' do
|
|
311
|
+
result = google_compute_helper.add_instance_tag('test-instance', 'new-tag')
|
|
312
|
+
expect(result).to be_a(MockOperation)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
describe '#set_metadata_on_instance' do
|
|
317
|
+
let(:metadata) { [{ key: 'test-key', value: 'test-value' }] }
|
|
318
|
+
|
|
319
|
+
before do
|
|
320
|
+
allow(compute_service).to receive_messages(
|
|
321
|
+
get_instance: MockInstance.new,
|
|
322
|
+
set_instance_metadata: MockOperation.new,
|
|
323
|
+
wait_zone_operation: MockOperation.new,
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'sets metadata on an instance' do
|
|
328
|
+
result = google_compute_helper.set_metadata_on_instance('test-instance', metadata)
|
|
329
|
+
expect(result).to be_a(MockOperation)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
describe 'error handling' do
|
|
335
|
+
describe 'GoogleComputeError' do
|
|
336
|
+
it 'defines a custom error class' do
|
|
337
|
+
expect(Beaker::GoogleComputeHelper::GoogleComputeError).to be < StandardError
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
describe 'constants' do
|
|
343
|
+
it 'defines AUTH_URL constant' do
|
|
344
|
+
expect(described_class::AUTH_URL).to eq('https://www.googleapis.com/auth/compute')
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it 'defines API_VERSION constant' do
|
|
348
|
+
expect(described_class::API_VERSION).to eq('v1')
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'defines DEFAULT_ZONE_NAME constant' do
|
|
352
|
+
expect(described_class::DEFAULT_ZONE_NAME).to eq('us-central1-a')
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'defines DEFAULT_MACHINE_TYPE constant' do
|
|
356
|
+
expect(described_class::DEFAULT_MACHINE_TYPE).to eq('e2-standard-4')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it 'defines DEFAULT_NETWORK_NAME constant' do
|
|
360
|
+
expect(described_class::DEFAULT_NETWORK_NAME).to eq('default')
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it 'defines VALID_PROTOS constant' do
|
|
364
|
+
expect(described_class::VALID_PROTOS).to eq(%w[tcp udp icmp esp ah ipip sctp])
|
|
365
|
+
.and be_frozen
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Beaker::GoogleComputeHelper do
|
|
6
|
+
context 'when using utilities' do
|
|
7
|
+
let(:logger) { instance_double(Beaker::Logger) }
|
|
8
|
+
let(:base_options) { { logger: logger, gce_project: 'test-project' } }
|
|
9
|
+
let(:helper) { described_class.new(base_options.merge(gce_zone: 'us-central1-a')) }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
# Mock the authentication and service initialization
|
|
13
|
+
allow(Google::Auth).to receive(:get_application_default).and_return(double)
|
|
14
|
+
|
|
15
|
+
compute_service = double
|
|
16
|
+
allow(compute_service).to receive(:authorization=)
|
|
17
|
+
allow(compute_service).to receive(:request_options).and_return(Google::Apis::RequestOptions.new)
|
|
18
|
+
allow(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
|
|
19
|
+
|
|
20
|
+
oslogin_service = double
|
|
21
|
+
allow(oslogin_service).to receive(:authorization=)
|
|
22
|
+
allow(Google::Apis::OsloginV1::CloudOSLoginService).to receive(:new).and_return(oslogin_service)
|
|
23
|
+
|
|
24
|
+
client_options = double
|
|
25
|
+
allow(client_options).to receive(:application_name=)
|
|
26
|
+
allow(client_options).to receive(:application_version=)
|
|
27
|
+
allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'region extraction' do
|
|
31
|
+
let(:zone_test_cases) do
|
|
32
|
+
{
|
|
33
|
+
'us-central1-a' => 'us-central1',
|
|
34
|
+
'us-west1-b' => 'us-west1',
|
|
35
|
+
'europe-west1-c' => 'europe-west1',
|
|
36
|
+
'asia-east1-a' => 'asia-east1',
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'extracts region from zone using default_region method' do
|
|
41
|
+
expect(helper.default_region).to eq('us-central1')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'handles different zone formats' do
|
|
45
|
+
zone_test_cases.each do |zone, expected_region|
|
|
46
|
+
helper_with_zone = described_class.allocate
|
|
47
|
+
helper_with_zone.instance_variable_set(:@options, base_options.merge(gce_zone: zone))
|
|
48
|
+
expect(helper_with_zone.send(:default_region)).to eq(expected_region)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'connection preferences' do
|
|
54
|
+
it 'prefers IP connections' do
|
|
55
|
+
google_compute = Beaker::GoogleCompute.new([], base_options)
|
|
56
|
+
expect(google_compute.connection_preference(nil)).to eq([:ip])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe 'environment variable handling' do
|
|
61
|
+
it 'uses default zone when environment variable is not set' do
|
|
62
|
+
# Test the actual default zone constant
|
|
63
|
+
expect(described_class::DEFAULT_ZONE_NAME).to eq('us-central1-a')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'uses default network when environment variable is not set' do
|
|
67
|
+
# Test the actual default network constant
|
|
68
|
+
expect(described_class::DEFAULT_NETWORK_NAME).to eq('default')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'processes port configuration through actual gem logic' do
|
|
72
|
+
helper_with_ports = described_class.new(
|
|
73
|
+
base_options.merge(gce_ports: ['80/tcp', '443/tcp', '22/tcp']),
|
|
74
|
+
)
|
|
75
|
+
expect(helper_with_ports.options[:gce_ports]).to eq(['80/tcp', '443/tcp', '22/tcp'])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'handles empty port configuration through actual gem logic' do
|
|
79
|
+
helper_with_empty_ports = described_class.new(base_options.merge(gce_ports: []))
|
|
80
|
+
expect(helper_with_empty_ports.options[:gce_ports]).to eq([])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe 'Windows image detection' do
|
|
85
|
+
it 'identifies Windows images correctly' do
|
|
86
|
+
expect(Beaker::GoogleCompute::WINDOWS_IMAGE_PROJECT).to include('windows-cloud')
|
|
87
|
+
.and include('windows-sql-cloud')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'excludes non-Windows images' do
|
|
91
|
+
expect(Beaker::GoogleCompute::WINDOWS_IMAGE_PROJECT).not_to include('ubuntu-os-cloud')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../../support/google_compute_shared_setup'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Beaker::GoogleCompute do
|
|
7
|
+
include GoogleComputeSharedSetup
|
|
8
|
+
|
|
9
|
+
setup_google_compute_mocks
|
|
10
|
+
setup_test_hosts
|
|
11
|
+
setup_test_options
|
|
12
|
+
|
|
13
|
+
subject(:google_compute) { described_class.new(hosts, options) }
|
|
14
|
+
|
|
15
|
+
describe '#initialize' do
|
|
16
|
+
it 'initializes hosts' do
|
|
17
|
+
expect(google_compute.instance_variable_get(:@hosts)).to eq(hosts)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'initializes options' do
|
|
21
|
+
expect(google_compute.instance_variable_get(:@options)).to eq(options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'initializes logger' do
|
|
25
|
+
expect(google_compute.instance_variable_get(:@logger)).to eq(logger)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'initializes external firewall name' do
|
|
29
|
+
expect(google_compute.instance_variable_get(:@external_firewall_name)).to eq('')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'initializes internal firewall name' do
|
|
33
|
+
expect(google_compute.instance_variable_get(:@internal_firewall_name)).to eq('')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#connection_preference' do
|
|
38
|
+
it 'returns IP preference' do
|
|
39
|
+
expect(google_compute.connection_preference(primary_host)).to eq([:ip])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'constants' do
|
|
44
|
+
it 'defines SLEEPWAIT constant' do
|
|
45
|
+
expect(described_class::SLEEPWAIT).to eq(5)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'defines WINDOWS_IMAGE_PROJECT constant' do
|
|
49
|
+
expect(described_class::WINDOWS_IMAGE_PROJECT).to eq(%w[windows-cloud windows-sql-cloud])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../../support/google_compute_shared_setup'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Beaker::GoogleCompute do
|
|
7
|
+
include GoogleComputeSharedSetup
|
|
8
|
+
|
|
9
|
+
setup_google_compute_mocks
|
|
10
|
+
setup_test_hosts
|
|
11
|
+
setup_test_options
|
|
12
|
+
|
|
13
|
+
subject(:google_compute) { described_class.new(hosts, options) }
|
|
14
|
+
|
|
15
|
+
describe '#provision' do
|
|
16
|
+
include FakeFS::SpecHelpers
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
allow(SecureRandom).to receive(:hex).and_return('abcd1234')
|
|
20
|
+
|
|
21
|
+
# Set up fake SSH keys for provision tests
|
|
22
|
+
private_key_path = File.join(Dir.home, '.ssh', 'google_compute_engine')
|
|
23
|
+
public_key_path = "#{private_key_path}.pub"
|
|
24
|
+
|
|
25
|
+
FileUtils.mkdir_p(File.dirname(private_key_path))
|
|
26
|
+
File.write(private_key_path, 'private key content')
|
|
27
|
+
File.write(public_key_path, 'ssh-rsa AAAAB3NzaC1yc2E...')
|
|
28
|
+
|
|
29
|
+
# Mock external SSH operations that are inherited from parent class
|
|
30
|
+
# These are only needed for provision tests and perform external SSH operations
|
|
31
|
+
# rubocop:disable RSpec/SubjectStub
|
|
32
|
+
allow(google_compute).to receive(:copy_ssh_to_root)
|
|
33
|
+
allow(google_compute).to receive(:enable_root_login)
|
|
34
|
+
# rubocop:enable RSpec/SubjectStub
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'provisions all hosts successfully' do
|
|
38
|
+
expect { google_compute.provision }.not_to raise_error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'sets vmhostname for all hosts' do
|
|
42
|
+
google_compute.provision
|
|
43
|
+
hosts.each do |host|
|
|
44
|
+
expect(host['vmhostname']).to match(/beaker-abcd1234-.*/)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'sets IP address for all hosts' do
|
|
49
|
+
google_compute.provision
|
|
50
|
+
hosts.each do |host|
|
|
51
|
+
expect(host['ip']).to eq('1.2.3.4')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#cleanup' do
|
|
57
|
+
before do
|
|
58
|
+
google_compute.instance_variable_set(:@external_firewall_name, 'external-fw')
|
|
59
|
+
google_compute.instance_variable_set(:@internal_firewall_name, 'internal-fw')
|
|
60
|
+
primary_host['vmhostname'] = 'vm-host1'
|
|
61
|
+
secondary_host['vmhostname'] = 'vm-host2'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'cleans up resources' do
|
|
65
|
+
expect { google_compute.cleanup }.not_to raise_error
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../../support/google_compute_shared_setup'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Beaker::GoogleCompute do
|
|
7
|
+
include GoogleComputeSharedSetup
|
|
8
|
+
|
|
9
|
+
setup_google_compute_mocks
|
|
10
|
+
setup_test_hosts
|
|
11
|
+
setup_test_options
|
|
12
|
+
|
|
13
|
+
subject(:google_compute) { described_class.new(hosts, options) }
|
|
14
|
+
|
|
15
|
+
describe '#find_google_ssh_private_key' do
|
|
16
|
+
include FakeFS::SpecHelpers
|
|
17
|
+
|
|
18
|
+
let(:default_private_key) { File.join(Dir.home, '.ssh', 'google_compute_engine') }
|
|
19
|
+
let(:custom_private_key) { '/custom/path/to/key' }
|
|
20
|
+
|
|
21
|
+
context 'when default key exists' do
|
|
22
|
+
before do
|
|
23
|
+
FileUtils.mkdir_p(File.dirname(default_private_key))
|
|
24
|
+
File.write(default_private_key, 'private key content')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns the default private key path' do
|
|
28
|
+
expect(google_compute.find_google_ssh_private_key).to eq(default_private_key)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'sets the private key in options' do
|
|
32
|
+
google_compute.find_google_ssh_private_key
|
|
33
|
+
expect(google_compute.instance_variable_get(:@options)[:gce_ssh_private_key]).to eq(default_private_key)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'when custom key is specified in options' do
|
|
38
|
+
before do
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(custom_private_key))
|
|
40
|
+
File.write(custom_private_key, 'private key content')
|
|
41
|
+
google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = custom_private_key
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns the custom private key path' do
|
|
45
|
+
expect(google_compute.find_google_ssh_private_key).to eq(custom_private_key)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'when private key does not exist' do
|
|
50
|
+
it 'raises an error' do
|
|
51
|
+
expect { google_compute.find_google_ssh_private_key }.to raise_error(/Could not find GCE Private SSH key/)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#find_google_ssh_public_key' do
|
|
57
|
+
include FakeFS::SpecHelpers
|
|
58
|
+
|
|
59
|
+
let(:private_key) { '/path/to/private_key' }
|
|
60
|
+
let(:public_key) { '/path/to/private_key.pub' }
|
|
61
|
+
|
|
62
|
+
before do
|
|
63
|
+
FileUtils.mkdir_p(File.dirname(private_key))
|
|
64
|
+
File.write(private_key, 'private key content')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context 'when public key exists' do
|
|
68
|
+
before do
|
|
69
|
+
File.write(public_key, 'public key content')
|
|
70
|
+
# Set up the private key path in options for this test
|
|
71
|
+
google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = private_key
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'returns the public key path' do
|
|
75
|
+
expect(google_compute.find_google_ssh_public_key).to eq(public_key)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'sets the public key in options' do
|
|
79
|
+
google_compute.find_google_ssh_public_key
|
|
80
|
+
expect(google_compute.instance_variable_get(:@options)[:gce_ssh_public_key]).to eq(public_key)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context 'when public key does not exist' do
|
|
85
|
+
before do
|
|
86
|
+
# Set up the private key path in options for this test
|
|
87
|
+
google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = private_key
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'raises an error' do
|
|
91
|
+
expect { google_compute.find_google_ssh_public_key }.to raise_error(/Could not find GCE Public SSH key/)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|