diff --git a/spec/tasks/abs_spec.rb b/spec/tasks/abs_spec.rb index bc059dd..8c858e4 100644 --- a/spec/tasks/abs_spec.rb +++ b/spec/tasks/abs_spec.rb @@ -26,10 +26,11 @@ include_context('with tmpdir') def with_env(env_vars) + original = env_vars.keys.each_with_object({}) { |k, h| h[k] = ENV.fetch(k, nil) } env_vars.each { |k, v| ENV[k] = v } yield ensure - env_vars.each { |k, _v| ENV.delete(k) } + original.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v } end before(:each) do @@ -71,6 +72,27 @@ def with_env(env_vars) end, ) end + + it 'raises when both node_name and platform are given for tear_down' do + expect($stdin).to receive(:read).and_return('{"action":"tear_down","node_name":"foo","platform":"bar"}') + expect { ABSProvision.run }.to raise_error(RuntimeError, %r{specify only a node_name}) + end + + it 'raises when both node_name and platform are given for provision' do + expect($stdin).to receive(:read).and_return('{"action":"provision","node_name":"foo","platform":"bar"}') + expect { ABSProvision.run }.to raise_error(RuntimeError, %r{specify only a platform}) + end + + it 'outputs error and exits 1 when task raises' do + expect($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') + runner = ABSProvision.new + allow(ABSProvision).to receive(:new).and_return(runner) + allow(runner).to receive(:task).and_raise(StandardError, 'network error') + expect { ABSProvision.run }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(%r{abs_failure}).to_stdout), + ) + end end context 'when provisioning' do @@ -121,6 +143,58 @@ def with_env(env_vars) end it 'raises an error if abs returns error response' + + it 'uses AppVeyor job URL when running in AppVeyor CI' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .with { |request| JSON.parse(request.body).dig('job', 'tags', 'jenkins_build_url') == 'https://ci.appveyor.com/project/org/repo/build/job/123' } + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('CI' => 'True', 'APPVEYOR' => 'True', 'APPVEYOR_REPO_NAME' => 'org/repo', 'APPVEYOR_JOB_ID' => '123') do + expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) + end + end + + it 'uses GitHub Actions job URL when running in GitHub Actions' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('GITHUB_ACTIONS' => 'true', 'GITHUB_REPOSITORY' => 'org/repo', 'GITHUB_RUN_ID' => '456') do + expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) + end + end + + it 'uses litmus_manual URL when not in a known CI environment' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('CI' => nil, 'TRAVIS' => nil, 'APPVEYOR' => nil, 'GITHUB_ACTIONS' => nil) do + expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) + end + end + + it 'uses ABS_SSH_PRIVATE_KEY when set' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('ABS_SSH_PRIVATE_KEY' => '/path/to/key') do + abs.task(**params) + end + target = YAML.load_file(inventory_file)['groups'].find { |g| g['name'] == 'ssh_nodes' }['targets'].first + expect(target.dig('config', 'ssh', 'private-key')).to eq('/path/to/key') + end + + it 'provisions a Windows platform into winrm_nodes with vars' do + windows_response = [{ 'type' => 'windows-2019-x86_64', 'hostname' => 'win-host.test' }] + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: windows_response.to_json }) + windows_params = params.merge(platform: 'windows-2019-x86_64', vars: 'role: agent_win') + abs.task(**windows_params) + targets = YAML.load_file(inventory_file)['groups'].find { |g| g['name'] == 'winrm_nodes' }['targets'] + expect(targets.size).to eq(1) + expect(targets.first['vars']).to eq({ 'role' => 'agent_win' }) + end + + it 'handles a Hash platform' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + expect(abs.task(action: 'provision', platform: { 'redhat-8-x86_64' => 1 }, inventory: inventory_file)).to eq({ status: 'ok', nodes: 1 }) + end end context 'when tearing down' do diff --git a/spec/tasks/provision_service_spec.rb b/spec/tasks/provision_service_spec.rb index 33d9c46..7695ed2 100644 --- a/spec/tasks/provision_service_spec.rb +++ b/spec/tasks/provision_service_spec.rb @@ -62,6 +62,22 @@ ) end end + + it 'calls provision and exits 0' do + allow($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') + runner = ProvisionService.new + allow(ProvisionService).to receive(:new).and_return(runner) + expect(runner).to receive(:provision).with('centos-8', instance_of(InventoryHelper), nil, nil).and_return({ status: 'ok', node_name: 'centos-8' }) + expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } + end + + it 'calls tear_down and exits 0' do + allow($stdin).to receive(:read).and_return('{"action":"tear_down","node_name":"some-node"}') + runner = ProvisionService.new + allow(ProvisionService).to receive(:new).and_return(runner) + allow(runner).to receive(:tear_down).and_return('{}') + expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } + end end describe '#provision' do @@ -150,4 +166,51 @@ end end end + + describe '#platform_to_cloud_request_parameters' do + let(:svc) { ProvisionService.new } + + it 'handles an Array platform' do + result = svc.platform_to_cloud_request_parameters(['centos-8', 'ubuntu-20'], 'aws', 'us-east-1', 'a') + expect(result[:images]).to eq(['centos-8', 'ubuntu-20']) + end + + it 'handles a Hash platform and wraps String images in an Array' do + platform = { cloud: 'gcp', images: 'centos-8' } + result = svc.platform_to_cloud_request_parameters(platform, nil, nil, nil) + expect(result[:images]).to eq(['centos-8']) + end + + it 'handles a Hash platform and overrides the cloud' do + platform = { images: ['centos-8'] } + result = svc.platform_to_cloud_request_parameters(platform, 'aws', nil, nil) + expect(result[:cloud]).to eq('aws') + end + end + + describe '#invoke_cloud_request' do + let(:svc) { ProvisionService.new } + let(:uri) { URI.parse('https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision') } + + it 'sends a DELETE request' do + stub_request(:delete, uri.to_s).to_return(status: 200, body: '{}') + result = svc.invoke_cloud_request('some-uuid', uri, nil, 'delete', 0) + expect(result).to eq('{}') + end + + it 'exits 1 with parsed JSON error details on a non-200 response' do + stub_request(:post, uri.to_s).to_return(status: 500, body: '{"error":"server error"}') + expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(include('"body":{"error":"server error"}', '"body_json":true')).to_stdout), + ) + end + + it 'exits 1 with raw body error details on a non-200 non-JSON response' do + stub_request(:post, uri.to_s).to_return(status: 500, body: 'Internal Server Error') + expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) }, + ) + end + end end diff --git a/spec/tasks/vagrant_spec.rb b/spec/tasks/vagrant_spec.rb index 4e0bef2..be157ca 100644 --- a/spec/tasks/vagrant_spec.rb +++ b/spec/tasks/vagrant_spec.rb @@ -33,4 +33,70 @@ include('"status":"ok"', '"platform":"generic/debian10"', '"role":"worker1"'), ).to_stdout end + + describe '#vagrant_version' do + it 'returns the parsed vagrant version' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]) + expect(vagrant_version).to eq(Gem::Version.new('2.3.4')) + end + + it 'memoizes the result' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]).once + vagrant_version + vagrant_version + end + end + + describe '#supports_windows_platform?' do + it 'returns true for vagrant >= 2.2.0' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.0', '', 0]) + expect(supports_windows_platform?).to be true + end + end + + describe '#generate_vagrantfile' do + before(:each) { allow(File).to receive(:read).and_call_original } + + it 'includes a provider config block with cpus when specified' do + path = File.join(tmpdir, 'Vagrantfile') + generate_vagrantfile(path, 'ubuntu-20.04', false, nil, 2, nil, nil, nil, nil, nil) + content = File.read(path) + expect(content).to include('v.cpus = 2') + end + + it 'includes box_url when specified, substituting the platform name' do + path = File.join(tmpdir, 'Vagrantfile') + generate_vagrantfile(path, 'ubuntu-20.04', false, 'virtualbox', nil, nil, nil, nil, nil, 'https://example.com/%BOX%.box') + content = File.read(path) + expect(content).to include("config.vm.box_url = 'https://example.com/ubuntu-20.04.box'") + end + end + + describe 'validation errors' do + it 'raises when both node_name and platform given for provision' do + allow($stdin).to receive(:read).and_return({ action: 'provision', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, %r{specify only a platform}) + end + + it 'raises when both node_name and platform given for tear_down' do + allow($stdin).to receive(:read).and_return({ action: 'tear_down', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, %r{specify only a node_name}) + end + + it 'raises when both node_name and platform given for an unknown action' do + allow($stdin).to receive(:read).and_return({ action: 'other', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, %r{specify only one of}) + end + end + + describe 'rescue' do + it 'exits 1 with an error JSON when provision raises' do + allow($stdin).to receive(:read).and_return({ platform: 'generic/ubuntu20', action: 'provision', inventory: tmpdir }.to_json) + allow(Open3).to receive(:capture3).with(%r{vagrant up}, any_args).and_return(['', 'vagrant failed!', 1]) + expect { vagrant }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(%r{vagrant_failure}).to_stdout), + ) + end + end end diff --git a/spec/unit/task_helper_spec.rb b/spec/unit/task_helper_spec.rb index f6a02a4..873e2b6 100644 --- a/spec/unit/task_helper_spec.rb +++ b/spec/unit/task_helper_spec.rb @@ -20,4 +20,19 @@ expect(platform_is_windows?('redhat8')).to be_falsey end end + + describe '.token_from_fogfile' do + it 'returns nil and prints a warning when the fog file does not exist' do + allow(File).to receive(:file?).and_return(false) + result = nil + expect { result = token_from_fogfile }.to output(%r{Cannot file fog file}).to_stdout + expect(result).to be_nil + end + + it 'prints a warning and returns nil when reading the fog file raises an error' do + allow(File).to receive(:file?).and_return(true) + allow(YAML).to receive(:load_file).and_raise(StandardError, 'bad file') + expect { token_from_fogfile }.to output(%r{Failed to get token}).to_stdout + end + end end