beaker-google 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/release.yml +41 -0
  3. data/.github/workflows/ci.yml +30 -17
  4. data/.github/workflows/release.yml +91 -17
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +4 -518
  7. data/.rubocop_todo.yml +20 -0
  8. data/.simplecov +2 -0
  9. data/CHANGELOG.md +50 -1
  10. data/Gemfile +13 -2
  11. data/README.md +2 -2
  12. data/Rakefile +23 -6
  13. data/beaker-google.gemspec +27 -23
  14. data/bin/beaker-google +1 -0
  15. data/lib/beaker/hypervisor/google.rb +5 -1
  16. data/lib/beaker/hypervisor/google_compute.rb +55 -25
  17. data/lib/beaker/hypervisor/google_compute_helper.rb +599 -581
  18. data/lib/beaker-google/version.rb +3 -1
  19. data/spec/classes/beaker/google_compute_constants_and_defaults_spec.rb +141 -0
  20. data/spec/classes/beaker/google_compute_helper_api_version_spec.rb +37 -0
  21. data/spec/classes/beaker/google_compute_helper_configuration_validation_spec.rb +100 -0
  22. data/spec/classes/beaker/google_compute_helper_constants_and_defaults_spec.rb +66 -0
  23. data/spec/classes/beaker/google_compute_helper_edge_cases_spec.rb +173 -0
  24. data/spec/classes/beaker/google_compute_helper_error_handling_spec.rb +105 -0
  25. data/spec/classes/beaker/google_compute_helper_spec.rb +368 -0
  26. data/spec/classes/beaker/google_compute_helper_utilities_spec.rb +95 -0
  27. data/spec/classes/beaker/google_compute_initialization_spec.rb +52 -0
  28. data/spec/classes/beaker/google_compute_provision_spec.rb +68 -0
  29. data/spec/classes/beaker/google_compute_spec.rb +95 -0
  30. data/spec/classes/beaker/google_compute_ssh_key_management_spec.rb +115 -0
  31. data/spec/classes/beaker/google_spec.rb +68 -0
  32. data/spec/classes/beaker-google/beaker_google_version_spec.rb +19 -0
  33. data/spec/classes/beaker_integration_spec.rb +67 -0
  34. data/spec/spec_helper.rb +45 -0
  35. data/spec/support/google_compute_shared_setup.rb +138 -0
  36. data/spec/support/shared_mocks.rb +89 -0
  37. metadata +102 -38
  38. data/.github/workflows/codeql-analysis.yml +0 -70
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Beaker::GoogleCompute do
6
+ include FakeFS::SpecHelpers
7
+
8
+ subject(:google_compute) do
9
+ mock_google_services
10
+ described_class.new(hosts, options)
11
+ end
12
+
13
+ let(:logger) { instance_double(Beaker::Logger) }
14
+ let(:hosts) { [] }
15
+ let(:options) { { logger: logger, gce_project: 'test-project', gce_ports: [] } }
16
+
17
+ def mock_google_services
18
+ # Mock the authentication and service initialization
19
+ allow(Google::Auth).to receive(:get_application_default).and_return(double)
20
+
21
+ compute_service = double
22
+ allow(compute_service).to receive(:authorization=)
23
+ allow(compute_service).to receive(:request_options).and_return(Google::Apis::RequestOptions.new)
24
+ allow(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
25
+
26
+ oslogin_service = double
27
+ allow(oslogin_service).to receive(:authorization=)
28
+ allow(Google::Apis::OsloginV1::CloudOSLoginService).to receive(:new).and_return(oslogin_service)
29
+
30
+ client_options = double
31
+ allow(client_options).to receive(:application_name=)
32
+ allow(client_options).to receive(:application_version=)
33
+ allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
34
+ end
35
+
36
+ describe '#find_google_ssh_private_key' do
37
+ let(:default_private_key) { File.join(Dir.home, '.ssh', 'google_compute_engine') }
38
+
39
+ context 'when default key exists' do
40
+ before do
41
+ FileUtils.mkdir_p(File.dirname(default_private_key))
42
+ File.write(default_private_key, 'private key content')
43
+ end
44
+
45
+ it 'returns the default private key path' do
46
+ expect(google_compute.find_google_ssh_private_key).to eq(default_private_key)
47
+ end
48
+ end
49
+
50
+ context 'when private key does not exist' do
51
+ it 'raises an error' do
52
+ expect { google_compute.find_google_ssh_private_key }.to raise_error(/Could not find GCE Private SSH key/)
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '#find_google_ssh_private_key with custom key' do
58
+ subject(:google_compute) do
59
+ mock_google_services
60
+ described_class.new(hosts, options.merge(gce_ssh_private_key: custom_private_key))
61
+ end
62
+
63
+ let(:custom_private_key) { '/custom/path/to/key' }
64
+
65
+ context 'when custom key is specified in options' do
66
+ before do
67
+ FileUtils.mkdir_p(File.dirname(custom_private_key))
68
+ File.write(custom_private_key, 'private key content')
69
+ end
70
+
71
+ it 'returns the custom private key path' do
72
+ expect(google_compute.find_google_ssh_private_key).to eq(custom_private_key)
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#find_google_ssh_public_key' do
78
+ subject(:google_compute) do
79
+ mock_google_services
80
+ described_class.new(hosts, options)
81
+ end
82
+
83
+ context 'when public key exists' do
84
+ let(:private_key) { '/path/to/private_key' }
85
+ let(:public_key) { '/path/to/private_key.pub' }
86
+
87
+ before do
88
+ FileUtils.mkdir_p(File.dirname(private_key))
89
+ File.write(private_key, 'private key content')
90
+ File.write(public_key, 'public key content')
91
+ # Set up the private key path in options for this test
92
+ google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = private_key
93
+ end
94
+
95
+ it 'returns the public key path' do
96
+ expect(google_compute.find_google_ssh_public_key).to eq(public_key)
97
+ end
98
+ end
99
+
100
+ context 'when public key does not exist' do
101
+ let(:private_key) { '/path/to/private_key' }
102
+
103
+ before do
104
+ FileUtils.mkdir_p(File.dirname(private_key))
105
+ File.write(private_key, 'private key content')
106
+ # Set up the private key path in options for this test
107
+ google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = private_key
108
+ end
109
+
110
+ it 'raises an error' do
111
+ expect { google_compute.find_google_ssh_public_key }.to raise_error(/Could not find GCE Public SSH key/)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # Mock the Google Compute classes to avoid dependency issues
6
+ module Beaker
7
+ class Google < Beaker::GoogleCompute
8
+ end
9
+ end
10
+
11
+ RSpec.describe Beaker::Google do
12
+ let(:hosts) { [] }
13
+ let(:options) { { gce_project: 'test', logger: instance_double(Beaker::Logger) } }
14
+
15
+ # Mock Google authentication and services to avoid actual authentication
16
+ before do
17
+ # Mock the authentication components
18
+ allow(Google::Auth).to receive(:get_application_default).and_return(
19
+ instance_double(Google::Auth::ServiceAccountCredentials),
20
+ )
21
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:from_env).and_return(
22
+ instance_double(Google::Auth::ServiceAccountCredentials),
23
+ )
24
+
25
+ # Mock the Google API client options
26
+ client_options = instance_double(Google::Apis::ClientOptions)
27
+ allow(client_options).to receive(:application_name=)
28
+ allow(client_options).to receive(:application_version=)
29
+ allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
30
+
31
+ # Mock the Google Compute and OS Login services
32
+ compute_service = instance_double(Google::Apis::ComputeV1::ComputeService)
33
+ allow(compute_service).to receive(:authorization=)
34
+ allow(compute_service).to receive(:request_options).and_return(Google::Apis::RequestOptions.new)
35
+ allow(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
36
+
37
+ oslogin_service = instance_double(Google::Apis::OsloginV1::CloudOSLoginService)
38
+ allow(oslogin_service).to receive(:authorization=)
39
+ allow(Google::Apis::OsloginV1::CloudOSLoginService).to receive(:new).and_return(oslogin_service)
40
+
41
+ # Mock environment variables that might be checked
42
+ allow(ENV).to receive(:[]).and_call_original
43
+ allow(ENV).to receive(:fetch).and_call_original
44
+ allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return(nil)
45
+ allow(ENV).to receive(:[]).with('GOOGLE_CLOUD_UNIVERSE_DOMAIN').and_return(nil)
46
+ allow(ENV).to receive(:[]).with('GOOGLE_CLOUD_PROJECT').and_return(nil)
47
+ allow(ENV).to receive(:[]).with('GCLOUD_PROJECT').and_return(nil)
48
+ allow(ENV).to receive(:[]).with('BEAKER_gce_project').and_return(nil)
49
+ allow(ENV).to receive(:[]).with('BEAKER_gce_ssh_public_key').and_return(nil)
50
+ allow(ENV).to receive(:[]).with('BEAKER_gce_ssh_private_key').and_return(nil)
51
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_zone', anything).and_return('us-central1-a')
52
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_network', anything).and_return('default')
53
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_subnetwork', anything).and_return(nil)
54
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_ports', anything).and_return('')
55
+ end
56
+
57
+ describe 'class hierarchy' do
58
+ it 'inherits from GoogleCompute' do
59
+ expect(described_class.superclass).to eq(Beaker::GoogleCompute)
60
+ end
61
+ end
62
+
63
+ describe 'instantiation' do
64
+ it 'can be instantiated' do
65
+ expect { described_class.new(hosts, options) }.not_to raise_error
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe BeakerGoogle do
6
+ describe 'VERSION' do
7
+ it 'has a version number' do
8
+ expect(BeakerGoogle::VERSION).not_to be_nil
9
+ end
10
+
11
+ it 'is a string' do
12
+ expect(BeakerGoogle::VERSION).to be_a(String)
13
+ end
14
+
15
+ it 'follows semantic versioning pattern' do
16
+ expect(BeakerGoogle::VERSION).to match(/\A\d+\.\d+\.\d+\z/)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Beaker do
6
+ context 'when testing Google integration' do
7
+ describe 'basic integration test' do
8
+ let(:logger) { instance_double(Beaker::Logger) }
9
+ let(:test_options) { { logger: logger, gce_project: 'test-project', gce_ports: [] } }
10
+
11
+ def mock_google_services
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
+ before do
31
+ mock_google_services
32
+ end
33
+
34
+ it 'loads the beaker-google version' do
35
+ expect(BeakerGoogle::VERSION).to be_a(String)
36
+ end
37
+
38
+ it 'can create GoogleComputeHelper instances' do
39
+ expect do
40
+ Beaker::GoogleComputeHelper.new(test_options)
41
+ end.not_to raise_error
42
+ end
43
+
44
+ it 'can create GoogleCompute instances' do
45
+ expect do
46
+ Beaker::GoogleCompute.new([], test_options)
47
+ end.not_to raise_error
48
+ end
49
+
50
+ it 'has proper class inheritance' do
51
+ expect(Beaker::GoogleCompute).to be < Beaker::Hypervisor
52
+ end
53
+
54
+ it 'loads ComputeV1 API' do
55
+ expect(defined?(Google::Apis::ComputeV1)).to be_truthy
56
+ end
57
+
58
+ it 'loads OsloginV1 API' do
59
+ expect(defined?(Google::Apis::OsloginV1)).to be_truthy
60
+ end
61
+
62
+ it 'loads Google Auth' do
63
+ expect(defined?(Google::Auth)).to be_truthy
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require 'rspec/its'
5
+ require 'fakefs/spec_helpers'
6
+ require 'simplecov'
7
+
8
+ # Start SimpleCov if coverage is enabled
9
+ SimpleCov.start if ENV['BEAKER_GOOGLE_COVERAGE']
10
+
11
+ # Require beaker first to establish the base classes
12
+ require 'beaker'
13
+
14
+ # Only require the version module directly
15
+ require 'beaker-google/version'
16
+
17
+ # Require the hypervisor classes
18
+ require 'beaker/hypervisor/google_compute_helper'
19
+ require 'beaker/hypervisor/google_compute'
20
+ require 'beaker/hypervisor/google'
21
+
22
+ # Load shared mock classes
23
+ require_relative 'support/shared_mocks'
24
+
25
+ RSpec.configure do |config|
26
+ config.expect_with :rspec do |expectations|
27
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
28
+ end
29
+
30
+ config.mock_with :rspec do |mocks|
31
+ mocks.verify_partial_doubles = true
32
+ end
33
+
34
+ config.shared_context_metadata_behavior = :apply_to_host_groups
35
+ config.filter_run_when_matching :focus
36
+ config.disable_monkey_patching!
37
+ config.warnings = true
38
+
39
+ config.default_formatter = 'doc' if config.files_to_run.one?
40
+
41
+ config.profile_examples = 10
42
+ config.order = :random
43
+
44
+ Kernel.srand config.seed
45
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared setup for Google Compute tests to avoid multiple memoized helpers
4
+ module GoogleComputeSharedSetup
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def setup_google_compute_mocks
11
+ let(:mock_gce_helper) { instance_double(Beaker::GoogleComputeHelper) }
12
+ let(:mock_network) { MockNetwork.new }
13
+ let(:mock_machine_type) { MockMachineType.new }
14
+ let(:mock_image) { MockImage.new }
15
+ let(:mock_instance) do
16
+ instance_double(Google::Apis::ComputeV1::Instance).tap do |instance|
17
+ allow(instance).to receive(:network_interfaces)
18
+ .and_return([
19
+ instance_double(Google::Apis::ComputeV1::NetworkInterface).tap do |interface|
20
+ allow(interface).to receive(:access_configs)
21
+ .and_return([
22
+ instance_double(Google::Apis::ComputeV1::AccessConfig).tap do |config|
23
+ allow(config).to receive(:nat_ip).and_return('1.2.3.4')
24
+ end,
25
+ ])
26
+ end,
27
+ ])
28
+ end
29
+ end
30
+ let(:mock_operation) do
31
+ instance_double(Google::Apis::ComputeV1::Operation).tap do |operation|
32
+ allow(operation).to receive(:error).and_return(nil)
33
+ end
34
+ end
35
+
36
+ let(:logger) { instance_double(Beaker::Logger, debug: nil, info: nil, warn: nil, error: nil) }
37
+
38
+ before do
39
+ # Mock the GoogleComputeHelper initialization to avoid authentication
40
+ allow(Beaker::GoogleComputeHelper).to receive(:new).and_return(mock_gce_helper)
41
+
42
+ # Mock all the GoogleComputeHelper methods
43
+ allow(mock_gce_helper).to receive_messages(
44
+ get_network: mock_network,
45
+ get_machine_type: mock_machine_type,
46
+ get_image: mock_image,
47
+ get_latest_image_from_family: mock_image,
48
+ get_instance: mock_instance,
49
+ create_firewall: mock_operation,
50
+ delete_firewall: mock_operation,
51
+ create_instance: mock_operation,
52
+ delete_instance: mock_operation,
53
+ add_instance_tag: mock_operation,
54
+ set_metadata_on_instance: mock_operation,
55
+ options: options,
56
+ )
57
+
58
+ # Set up environment variables for testing
59
+ allow(ENV).to receive(:fetch).and_call_original
60
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_machine_type', anything).and_return('n1-standard-1')
61
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_ssh_private_key',
62
+ anything).and_return(File.join(Dir.home, '.ssh', 'google_compute_engine'))
63
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_zone', anything).and_return('us-central1-a')
64
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_network', anything).and_return('default')
65
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_subnetwork', anything).and_return(nil)
66
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_ports', anything).and_return('80/tcp,443/tcp')
67
+ allow(ENV).to receive(:[]).with('BEAKER_gce_project').and_return('test-project')
68
+ allow(ENV).to receive(:[]).with('BEAKER_gce_ssh_public_key').and_return(nil)
69
+ allow(ENV).to receive(:[]).with('BEAKER_gce_ssh_private_key').and_return(nil)
70
+
71
+ # Mock the image disk size
72
+ allow(mock_image).to receive(:disk_size_gb).and_return(20)
73
+ end
74
+ end
75
+
76
+ def setup_test_hosts
77
+ let(:primary_host) do
78
+ {
79
+ 'name' => 'test-host-1',
80
+ 'user' => 'root',
81
+ 'options' => { 'ssh' => { 'keys' => [] } },
82
+ 'gce_machine_type' => 'n1-standard-1',
83
+ :image => 'ubuntu-1804-lts',
84
+ 'volume_size' => nil,
85
+ 'disable_root_ssh' => nil,
86
+ }.tap do |host|
87
+ # Add methods needed by the gem
88
+ def host.close; end
89
+
90
+ def host.name
91
+ self['name']
92
+ end
93
+
94
+ def host.options
95
+ self['options']
96
+ end
97
+ end
98
+ end
99
+
100
+ let(:secondary_host) do
101
+ {
102
+ 'name' => 'test-host-2',
103
+ 'user' => 'root',
104
+ 'options' => { 'ssh' => { 'keys' => [] } },
105
+ 'gce_machine_type' => 'n1-standard-1',
106
+ :image => 'ubuntu-1804-lts',
107
+ 'volume_size' => nil,
108
+ 'disable_root_ssh' => nil,
109
+ }.tap do |host|
110
+ # Add methods needed by the gem
111
+ def host.close; end
112
+
113
+ def host.name
114
+ self['name']
115
+ end
116
+
117
+ def host.options
118
+ self['options']
119
+ end
120
+ end
121
+ end
122
+
123
+ let(:hosts) { [primary_host, secondary_host] }
124
+ end
125
+
126
+ def setup_test_options
127
+ let(:options) do
128
+ {
129
+ gce_project: 'test-project',
130
+ gce_zone: 'us-central1-a',
131
+ gce_network: 'default',
132
+ gce_ports: ['80/tcp', '443/tcp'],
133
+ logger: logger,
134
+ }
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ # Add mutex_m requirement for Ruby 3.4.0 compatibility
6
+ begin
7
+ require 'mutex_m'
8
+ rescue LoadError
9
+ # mutex_m is not available on all Ruby versions
10
+ end
11
+
12
+ # Mock external classes that we don't want to test
13
+ class MockNetwork
14
+ def initialize
15
+ @name = 'test-network'
16
+ @self_link = 'https://www.googleapis.com/compute/v1/projects/test-project/global/networks/test-network'
17
+ end
18
+
19
+ attr_reader :name, :self_link
20
+ end
21
+
22
+ class MockSubnetwork
23
+ def initialize
24
+ @name = 'test-subnetwork'
25
+ @self_link = 'https://www.googleapis.com/compute/v1/projects/test-project/regions/us-central1/subnetworks/test-subnetwork'
26
+ end
27
+
28
+ attr_reader :name, :self_link
29
+ end
30
+
31
+ class MockMachineType
32
+ def initialize
33
+ @name = 'n1-standard-1'
34
+ @memory_mb = 3840
35
+ @guest_cpus = 1
36
+ end
37
+
38
+ attr_reader :name, :memory_mb, :guest_cpus
39
+ end
40
+
41
+ class MockImage
42
+ def initialize
43
+ @name = 'test-image'
44
+ @self_link = 'https://www.googleapis.com/compute/v1/projects/test-project/global/images/test-image'
45
+ @disk_size_gb = 20
46
+ end
47
+
48
+ attr_reader :name, :self_link, :disk_size_gb
49
+ end
50
+
51
+ class MockInstance
52
+ def initialize
53
+ @name = 'test-instance'
54
+ @status = 'RUNNING'
55
+ @tags = MockTags.new
56
+ @metadata = MockMetadata.new
57
+ @fingerprint = 'test-fingerprint'
58
+ end
59
+
60
+ attr_reader :name, :status, :tags, :metadata, :fingerprint
61
+ end
62
+
63
+ class MockTags
64
+ def initialize
65
+ @items = []
66
+ @fingerprint = 'test-fingerprint'
67
+ end
68
+
69
+ attr_reader :items, :fingerprint
70
+ end
71
+
72
+ class MockMetadata
73
+ def initialize
74
+ @items = []
75
+ @fingerprint = 'test-fingerprint'
76
+ end
77
+
78
+ attr_accessor :items
79
+ attr_reader :fingerprint
80
+ end
81
+
82
+ class MockOperation
83
+ def initialize
84
+ @name = 'test-operation'
85
+ @status = 'RUNNING'
86
+ end
87
+
88
+ attr_reader :name, :status
89
+ end