gitlab-ce/spec/models/ci/runner_manager_spec.rb

531 lines
16 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :model do
it_behaves_like 'having unique enum values'
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_runner_machine }
end
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
it { is_expected.to have_many(:runner_manager_builds) }
it { is_expected.to have_many(:builds).through(:runner_manager_builds) }
describe 'validation' do
it { is_expected.to validate_presence_of(:runner) }
it { is_expected.to validate_presence_of(:system_xid) }
it { is_expected.to validate_length_of(:system_xid).is_at_most(64) }
it { is_expected.to validate_length_of(:version).is_at_most(2048) }
it { is_expected.to validate_length_of(:revision).is_at_most(255) }
it { is_expected.to validate_length_of(:platform).is_at_most(255) }
it { is_expected.to validate_length_of(:architecture).is_at_most(255) }
it { is_expected.to validate_length_of(:ip_address).is_at_most(1024) }
context 'when runner has config' do
it 'is valid' do
runner_manager = build(:ci_runner_machine, config: { gpus: "all" })
expect(runner_manager).to be_valid
end
end
context 'when runner has an invalid config' do
it 'is invalid' do
runner_manager = build(:ci_runner_machine, config: { test: 1 })
expect(runner_manager).not_to be_valid
end
end
end
describe 'status scopes' do
let_it_be(:runner) { create(:ci_runner, :instance) }
let_it_be(:offline_runner_manager) { create(:ci_runner_machine, runner: runner, contacted_at: 2.hours.ago) }
let_it_be(:online_runner_manager) { create(:ci_runner_machine, runner: runner, contacted_at: 1.second.ago) }
let_it_be(:never_contacted_runner_manager) { create(:ci_runner_machine, runner: runner, contacted_at: nil) }
describe '.online' do
subject(:runner_managers) { described_class.online }
it 'returns online runner managers' do
expect(runner_managers).to contain_exactly(online_runner_manager)
end
end
describe '.offline' do
subject(:runner_managers) { described_class.offline }
it 'returns offline runner managers' do
expect(runner_managers).to contain_exactly(offline_runner_manager)
end
end
describe '.never_contacted' do
subject(:runner_managers) { described_class.never_contacted }
it 'returns never contacted runner managers' do
expect(runner_managers).to contain_exactly(never_contacted_runner_manager)
end
end
describe '.stale', :freeze_time do
subject { described_class.stale }
let!(:stale_runner_manager1) do
create(
:ci_runner_machine,
runner: runner,
created_at: described_class.stale_deadline - 1.second,
contacted_at: nil
)
end
let!(:stale_runner_manager2) do
create(
:ci_runner_machine,
runner: runner,
created_at: 8.days.ago,
contacted_at: described_class.stale_deadline - 1.second
)
end
it 'returns stale runner managers' do
is_expected.to contain_exactly(stale_runner_manager1, stale_runner_manager2)
end
end
include_examples 'runner with status scope'
end
describe '.available_statuses' do
subject { described_class.available_statuses }
it { is_expected.to eq(%w[online offline never_contacted stale]) }
end
describe '.online_contact_time_deadline', :freeze_time do
subject { described_class.online_contact_time_deadline }
it { is_expected.to eq(2.hours.ago) }
end
describe '.stale_deadline', :freeze_time do
subject { described_class.stale_deadline }
it { is_expected.to eq(7.days.ago) }
end
describe '.for_runner' do
subject(:runner_managers) { described_class.for_runner(runner_arg) }
let_it_be(:runner1) { create(:ci_runner) }
let_it_be(:runner_manager11) { create(:ci_runner_machine, runner: runner1) }
let_it_be(:runner_manager12) { create(:ci_runner_machine, runner: runner1) }
context 'with single runner' do
let(:runner_arg) { runner1 }
it { is_expected.to contain_exactly(runner_manager11, runner_manager12) }
end
context 'with multiple runners' do
let(:runner_arg) { [runner1, runner2] }
let_it_be(:runner2) { create(:ci_runner) }
let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner2) }
it { is_expected.to contain_exactly(runner_manager11, runner_manager12, runner_manager2) }
end
end
describe '.aggregate_upgrade_status_by_runner_id' do
let!(:runner_version1) { create(:ci_runner_version, version: '16.0.0', status: :recommended) }
let!(:runner_version2) { create(:ci_runner_version, version: '16.0.1', status: :available) }
let!(:runner1) { create(:ci_runner) }
let!(:runner2) { create(:ci_runner) }
let!(:runner_manager11) { create(:ci_runner_machine, runner: runner1, version: runner_version1.version) }
let!(:runner_manager12) { create(:ci_runner_machine, runner: runner1, version: runner_version2.version) }
let!(:runner_manager2) { create(:ci_runner_machine, runner: runner2, version: runner_version2.version) }
subject { described_class.aggregate_upgrade_status_by_runner_id }
it 'contains aggregate runner upgrade status by runner ID' do
is_expected.to eq({
runner1.id => :recommended,
runner2.id => :available
})
end
end
describe '.with_running_builds' do
subject(:scope) { described_class.with_running_builds }
let_it_be(:runner) { create(:ci_runner) }
let_it_be(:runner_manager1) { create(:ci_runner_machine, runner: runner) }
let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner) }
before_all do
create(:ci_runner_machine_build, runner_manager: runner_manager1,
build: create(:ci_build, :success, runner: runner))
create(:ci_runner_machine_build, runner_manager: runner_manager2,
build: create(:ci_build, :running, runner: runner))
end
it { is_expected.to contain_exactly runner_manager2 }
end
describe '.order_id_desc' do
subject(:scope) { described_class.order_id_desc }
let_it_be(:runner_manager1) { create(:ci_runner_machine) }
let_it_be(:runner_manager2) { create(:ci_runner_machine) }
specify { expect(described_class.all).to eq([runner_manager1, runner_manager2]) }
it { is_expected.to eq([runner_manager2, runner_manager1]) }
end
describe '#status', :freeze_time do
let(:runner_manager) { build(:ci_runner_machine, created_at: 8.days.ago) }
subject { runner_manager.status }
context 'if never connected' do
before do
runner_manager.contacted_at = nil
end
it { is_expected.to eq(:stale) }
context 'if created recently' do
before do
runner_manager.created_at = 1.day.ago
end
it { is_expected.to eq(:never_contacted) }
end
end
context 'if contacted 1s ago' do
before do
runner_manager.contacted_at = 1.second.ago
end
it { is_expected.to eq(:online) }
end
context 'if contacted recently' do
before do
runner_manager.contacted_at = 2.hours.ago
end
it { is_expected.to eq(:offline) }
end
context 'if contacted long time ago' do
before do
runner_manager.contacted_at = 7.days.ago
end
it { is_expected.to eq(:stale) }
end
end
describe '#heartbeat', :freeze_time do
let(:runner_manager) { create(:ci_runner_machine, version: '15.0.0') }
let(:executor) { 'shell' }
let(:values) do
{
ip_address: '8.8.8.8',
architecture: '18-bit',
config: { gpus: "all" },
executor: executor,
version: version
}
end
subject(:heartbeat) do
runner_manager.heartbeat(values)
end
context 'when database was updated recently' do
before do
runner_manager.contacted_at = Time.current
end
context 'when version is changed' do
let(:version) { '15.0.1' }
before do
allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
end
it 'schedules version information update' do
heartbeat
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
end
it 'updates cache' do
expect_redis_update
heartbeat
expect(runner_manager.runner_version).to be_nil
end
context 'when fetching runner releases is disabled' do
before do
stub_application_setting(update_runner_versions_enabled: false)
end
it 'does not schedule version information update' do
heartbeat
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
end
end
end
context 'with only ip_address specified' do
let(:values) do
{ ip_address: '1.1.1.1' }
end
it 'updates only ip_address' do
expect_redis_update(values.merge(contacted_at: Time.current))
heartbeat
end
context 'with new version having been cached' do
let(:version) { '15.0.1' }
before do
runner_manager.cache_attributes(version: version)
end
it 'does not lose cached version value' do
expect { heartbeat }.not_to change { runner_manager.version }.from(version)
end
end
end
end
context 'when database was not updated recently' do
before do
runner_manager.contacted_at = 2.hours.ago
allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
end
context 'when version is changed' do
let(:version) { '15.0.1' }
context 'with invalid runner_manager' do
before do
runner_manager.runner = nil
end
it 'still updates redis cache and database' do
expect(runner_manager).to be_invalid
expect_redis_update
does_db_update
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
.with(version).once
end
end
it 'updates redis cache and database' do
expect_redis_update
does_db_update
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
.with(version).once
end
end
context 'with unchanged runner_manager version' do
let(:version) { runner_manager.version }
it 'does not schedule ci_runner_versions update' do
heartbeat
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
end
Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
context "with #{executor} executor" do
let(:executor) { executor }
it 'updates with expected executor type' do
expect_redis_update
heartbeat
expect(runner_manager.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
end
def expected_executor_type
executor.gsub(/[+-]/, '_')
end
end
end
context 'with an unknown executor type' do
let(:executor) { 'some-unknown-type' }
it 'updates with unknown executor type' do
expect_redis_update
heartbeat
expect(runner_manager.reload.read_attribute(:executor_type)).to eq('unknown')
end
end
end
end
def expect_redis_update(values = anything)
values_json = values == anything ? anything : Gitlab::Json.dump(values)
Gitlab::Redis::Cache.with do |redis|
redis_key = runner_manager.send(:cache_attribute_key)
expect(redis).to receive(:set).with(redis_key, values_json, any_args).and_call_original
end
end
def does_db_update
expect { heartbeat }.to change { runner_manager.reload.read_attribute(:contacted_at) }
.and change { runner_manager.reload.read_attribute(:architecture) }
.and change { runner_manager.reload.read_attribute(:config) }
.and change { runner_manager.reload.read_attribute(:executor_type) }
end
end
describe '#builds' do
let_it_be(:runner_manager) { create(:ci_runner_machine) }
subject(:builds) { runner_manager.builds }
it { is_expected.to be_empty }
context 'with an existing build' do
let!(:build) { create(:ci_build) }
let!(:runner_machine_build) do
create(:ci_runner_machine_build, runner_manager: runner_manager, build: build)
end
it { is_expected.to contain_exactly build }
end
end
describe '.with_upgrade_status' do
subject(:scope) { described_class.with_upgrade_status(upgrade_status) }
let_it_be(:runner_manager_14_0_0) { create(:ci_runner_machine, version: '14.0.0') }
let_it_be(:runner_manager_14_1_0) { create(:ci_runner_machine, version: '14.1.0') }
let_it_be(:runner_manager_14_1_1) { create(:ci_runner_machine, version: '14.1.1') }
before_all do
create(:ci_runner_version, version: '14.0.0', status: :available)
create(:ci_runner_version, version: '14.1.0', status: :recommended)
create(:ci_runner_version, version: '14.1.1', status: :unavailable)
end
context 'as :unavailable' do
let(:upgrade_status) { :unavailable }
it 'returns runners with runner managers whose version is assigned :unavailable' do
is_expected.to contain_exactly(runner_manager_14_1_1)
end
end
context 'as :available' do
let(:upgrade_status) { :available }
it 'returns runners with runner managers whose version is assigned :available' do
is_expected.to contain_exactly(runner_manager_14_0_0)
end
end
context 'as :recommended' do
let(:upgrade_status) { :recommended }
it 'returns runners with runner managers whose version is assigned :recommended' do
is_expected.to contain_exactly(runner_manager_14_1_0)
end
end
end
describe '.with_version_prefix' do
subject { described_class.with_version_prefix(version_prefix) }
let_it_be(:runner_manager1) { create(:ci_runner_machine, version: '15.11.0') }
let_it_be(:runner_manager2) { create(:ci_runner_machine, version: '15.9.0') }
let_it_be(:runner_manager3) { create(:ci_runner_machine, version: '15.11.5') }
context 'with a prefix string of "15."' do
let(:version_prefix) { "15." }
it 'returns runner managers' do
is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3)
end
end
context 'with a prefix string of "15"' do
let(:version_prefix) { "15" }
it 'returns runner managers' do
is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3)
end
end
context 'with a prefix string of "15.11."' do
let(:version_prefix) { "15.11." }
it 'returns runner managers' do
is_expected.to contain_exactly(runner_manager1, runner_manager3)
end
end
context 'with a prefix string of "15.11"' do
let(:version_prefix) { "15.11" }
it 'returns runner managers' do
is_expected.to contain_exactly(runner_manager1, runner_manager3)
end
end
context 'with a prefix string of "15.9"' do
let(:version_prefix) { "15.9" }
it 'returns runner managers' do
is_expected.to contain_exactly(runner_manager2)
end
end
context 'with a prefix string of "15.11.5"' do
let(:version_prefix) { "15.11.5" }
it 'returns runner managers' do
is_expected.to contain_exactly(runner_manager3)
end
end
context 'with a malformed prefix of "V2"' do
let(:version_prefix) { "V2" }
it 'returns no runner managers' do
is_expected.to be_empty
end
end
end
end