diff options
Diffstat (limited to 'spec/requests/api/graphql/ci/runner_spec.rb')
-rw-r--r-- | spec/requests/api/graphql/ci/runner_spec.rb | 350 |
1 files changed, 248 insertions, 102 deletions
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 986e3ce9e52..52b548ce8b9 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -6,11 +6,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do include GraphqlHelpers let_it_be(:user) { create(:user, :admin) } - let_it_be(:group) { create(:group) } + let_it_be(:another_admin) { create(:user, :admin) } + let_it_be_with_reload(:group) { create(:group) } let_it_be(:active_instance_runner) do - create(:ci_runner, :instance, + create(:ci_runner, :instance, :with_runner_manager, description: 'Runner 1', + creator: user, contacted_at: 2.hours.ago, active: true, version: 'adfe156', @@ -28,6 +30,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let_it_be(:inactive_instance_runner) do create(:ci_runner, :instance, description: 'Runner 2', + creator: another_admin, contacted_at: 1.day.ago, active: false, version: 'adfe157', @@ -55,7 +58,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end let_it_be(:project1) { create(:project) } - let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) } + let_it_be(:active_project_runner) do + create(:ci_runner, :project, :with_runner_manager, projects: [project1]) + end shared_examples 'runner details fetch' do let(:query) do @@ -77,6 +82,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do expect(runner_data).to match a_graphql_entity_for( runner, description: runner.description, + created_by: runner.creator ? a_graphql_entity_for(runner.creator) : nil, created_at: runner.created_at&.iso8601, contacted_at: runner.contacted_at&.iso8601, version: runner.version, @@ -85,7 +91,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do locked: false, active: runner.active, paused: !runner.active, - status: runner.status('14.5').to_s.upcase, + status: runner.status.to_s.upcase, job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE', maximum_timeout: runner.maximum_timeout, access_level: runner.access_level.to_s.upcase, @@ -107,15 +113,39 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do ), project_count: nil, admin_url: "http://localhost/admin/runners/#{runner.id}", + edit_admin_url: "http://localhost/admin/runners/#{runner.id}/edit", + register_admin_url: runner.registration_available? ? "http://localhost/admin/runners/#{runner.id}/register" : nil, user_permissions: { 'readRunner' => true, 'updateRunner' => true, 'deleteRunner' => true, 'assignRunner' => true - } + }, + managers: a_hash_including( + "count" => runner.runner_managers.count, + "nodes" => an_instance_of(Array), + "pageInfo" => anything + ) ) expect(runner_data['tagList']).to match_array runner.tag_list end + + it 'does not execute more queries per runner', :use_sql_query_cache, :aggregate_failures do + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + expect(graphql_data_at(:runner)).not_to be_nil + + personal_access_token = create(:personal_access_token, user: another_admin) + args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) } + + create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: another_admin) + create(:ci_runner, :project, version: '14.0.1', projects: [project1], tag_list: %w[tag3 tag8], creator: another_admin) + + expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + end end shared_examples 'retrieval with no admin url' do @@ -135,7 +165,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do runner_data = graphql_data_at(:runner) expect(runner_data).not_to be_nil - expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil) + expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil, edit_admin_url: nil) expect(runner_data['tagList']).to match_array runner.tag_list end end @@ -307,6 +337,24 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do it_behaves_like 'runner details fetch' end + describe 'for registration type' do + context 'when registered with registration token' do + let(:runner) do + create(:ci_runner, registration_type: :registration_token) + end + + it_behaves_like 'runner details fetch' + end + + context 'when registered with authenticated user' do + let(:runner) do + create(:ci_runner, registration_type: :authenticated_user) + end + + it_behaves_like 'runner details fetch' + end + end + describe 'for group runner request' do let(:query) do %( @@ -330,24 +378,110 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end end - describe 'for runner with status' do - let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } - let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } - - let(:status_fragment) do + describe 'ephemeralRegisterUrl' do + let(:runner_args) { { registration_type: :authenticated_user, creator: creator } } + let(:query) do %( - status - legacyStatusWithExplicitVersion: status(legacyMode: "14.5") - newStatus: status(legacyMode: null) + query { + runner(id: "#{runner.to_global_id}") { + ephemeralRegisterUrl + } + } ) end + shared_examples 'has register url' do + it 'retrieves register url' do + post_graphql(query, current_user: user) + expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(expected_url) + end + end + + shared_examples 'has no register url' do + it 'retrieves no register url' do + post_graphql(query, current_user: user) + expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(nil) + end + end + + context 'with an instance runner', :freeze_time do + let(:creator) { user } + let(:runner) { create(:ci_runner, **runner_args) } + + context 'with valid ephemeral registration' do + it_behaves_like 'has register url' do + let(:expected_url) { "http://localhost/admin/runners/#{runner.id}/register" } + end + end + + context 'when runner ephemeral registration has expired' do + let(:runner) do + create(:ci_runner, created_at: (Ci::Runner::REGISTRATION_AVAILABILITY_TIME + 1.second).ago, **runner_args) + end + + it_behaves_like 'has no register url' + end + + context 'when runner has already been registered' do + let(:runner) { create(:ci_runner, :with_runner_manager, **runner_args) } + + it_behaves_like 'has no register url' + end + end + + context 'with a group runner' do + let(:creator) { user } + let(:runner) { create(:ci_runner, :group, groups: [group], **runner_args) } + + context 'with valid ephemeral registration' do + it_behaves_like 'has register url' do + let(:expected_url) { "http://localhost/groups/#{group.path}/-/runners/#{runner.id}/register" } + end + end + + context 'when request not from creator' do + let(:creator) { another_admin } + + before do + group.add_owner(another_admin) + end + + it_behaves_like 'has no register url' + end + end + + context 'with a project runner' do + let(:creator) { user } + let(:runner) { create(:ci_runner, :project, projects: [project1], **runner_args) } + + context 'with valid ephemeral registration' do + it_behaves_like 'has register url' do + let(:expected_url) { "http://localhost/#{project1.full_path}/-/runners/#{runner.id}/register" } + end + end + + context 'when request not from creator' do + let(:creator) { another_admin } + + before do + project1.add_owner(another_admin) + end + + it_behaves_like 'has no register url' + end + end + end + + describe 'for runner with status' do + let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } + let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } + let(:query) do %( query { - staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} } - pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} } - neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} } + staleRunner: runner(id: "#{stale_runner.to_global_id}") { status } + pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { status } + neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { status } } ) end @@ -357,23 +491,17 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do stale_runner_data = graphql_data_at(:stale_runner) expect(stale_runner_data).to match a_hash_including( - 'status' => 'STALE', - 'legacyStatusWithExplicitVersion' => 'STALE', - 'newStatus' => 'STALE' + 'status' => 'STALE' ) paused_runner_data = graphql_data_at(:paused_runner) expect(paused_runner_data).to match a_hash_including( - 'status' => 'PAUSED', - 'legacyStatusWithExplicitVersion' => 'PAUSED', - 'newStatus' => 'OFFLINE' + 'status' => 'OFFLINE' ) never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner) expect(never_contacted_instance_runner_data).to match a_hash_including( - 'status' => 'NEVER_CONTACTED', - 'legacyStatusWithExplicitVersion' => 'NEVER_CONTACTED', - 'newStatus' => 'NEVER_CONTACTED' + 'status' => 'NEVER_CONTACTED' ) end end @@ -568,34 +696,34 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end end - context 'with request made by creator' do + context 'with request made by creator', :frozen_time do let(:user) { creator } context 'with runner created in UI' do let(:registration_type) { :authenticated_user } - context 'with runner created in last 3 hours' do - let(:created_at) { (3.hours - 1.second).ago } + context 'with runner created in last hour' do + let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago } - context 'with no runner machine registed yet' do + context 'with no runner manager registered yet' do it_behaves_like 'an ephemeral_authentication_token' end - context 'with first runner machine already registed' do - let!(:runner_machine) { create(:ci_runner_machine, runner: runner) } + context 'with first runner manager already registered' do + let!(:runner_manager) { create(:ci_runner_machine, runner: runner) } it_behaves_like 'a protected ephemeral_authentication_token' end end context 'with runner created almost too long ago' do - let(:created_at) { (3.hours - 1.second).ago } + let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago } it_behaves_like 'an ephemeral_authentication_token' end context 'with runner created too long ago' do - let(:created_at) { 3.hours.ago } + let(:created_at) { Ci::Runner::REGISTRATION_AVAILABILITY_TIME.ago } it_behaves_like 'a protected ephemeral_authentication_token' end @@ -604,8 +732,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do context 'with runner registered from command line' do let(:registration_type) { :registration_token } - context 'with runner created in last 3 hours' do - let(:created_at) { (3.hours - 1.second).ago } + context 'with runner created in last 1 hour' do + let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago } it_behaves_like 'a protected ephemeral_authentication_token' end @@ -628,6 +756,12 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do <<~SINGLE runner(id: "#{runner.to_global_id}") { #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} + createdBy { + id + username + webPath + webUrl + } groups { nodes { id @@ -658,7 +792,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let(:active_group_runner2) { create(:ci_runner, :group) } # Exclude fields that are already hardcoded above - let(:excluded_fields) { %w[jobs groups projects ownerProject] } + let(:excluded_fields) { %w[createdBy jobs groups projects ownerProject] } let(:single_query) do <<~QUERY @@ -691,6 +825,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) } + personal_access_token = create(:personal_access_token, user: another_admin) + args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control) expect(graphql_data.count).to eq 6 @@ -721,20 +857,20 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end describe 'Query limits with jobs' do - let!(:group1) { create(:group) } - let!(:group2) { create(:group) } - let!(:project1) { create(:project, :repository, group: group1) } - let!(:project2) { create(:project, :repository, group: group1) } - let!(:project3) { create(:project, :repository, group: group2) } + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:project1) { create(:project, :repository, group: group1) } + let_it_be(:project2) { create(:project, :repository, group: group1) } + let_it_be(:project3) { create(:project, :repository, group: group2) } - let!(:merge_request1) { create(:merge_request, source_project: project1) } - let!(:merge_request2) { create(:merge_request, source_project: project3) } + let_it_be(:merge_request1) { create(:merge_request, source_project: project1) } + let_it_be(:merge_request2) { create(:merge_request, source_project: project3) } let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) } let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) } - let!(:pipeline1) do + let_it_be(:pipeline1) do create(:ci_pipeline, project: project1, source: :merge_request_event, merge_request: merge_request1, ref: 'main', - target_sha: 'xxx') + target_sha: 'xxx') end let(:query) do @@ -745,24 +881,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do jobs { nodes { id - detailedStatus { - id - detailsPath - group - icon - text - } - project { - id - name - webUrl - } - shortSha - commitPath - finishedAt - duration - queuedDuration - tags + #{field} } } } @@ -770,42 +889,69 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do QUERY end - it 'does not execute more queries per job', :aggregate_failures do - # warm-up license cache and so on: - personal_access_token = create(:personal_access_token, user: user) - args = { current_user: user, token: { personal_access_token: personal_access_token } } - post_graphql(query, **args) - - control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) } - - # Add a new build to project_runner2 - project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3) - pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2, - ref: 'main', target_sha: 'xxx') - build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2) + context 'when requesting individual fields' do + using RSpec::Parameterized::TableSyntax - args[:current_user] = create(:user, :admin) # do not reuse same user - expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + where(:field) do + [ + 'detailedStatus { id detailsPath group icon text }', + 'project { id name webUrl }' + ] + %w[ + shortSha + browseArtifactsPath + commitPath + playPath + refPath + webPath + finishedAt + duration + queuedDuration + tags + ] + end - expect(graphql_data.count).to eq 1 - expect(graphql_data).to match( - a_hash_including( - 'runner' => a_graphql_entity_for( - project_runner2, - jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) } - ) - )) + with_them do + it 'does not execute more queries per job', :use_sql_query_cache, :aggregate_failures do + admin2 = create(:user, :admin) # do not reuse same user + + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: user) + personal_access_token2 = create(:personal_access_token, user: admin2) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + args2 = { current_user: admin2, token: { personal_access_token: personal_access_token2 } } + post_graphql(query, **args2) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) } + + # Add a new build to project_runner2 + project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3) + pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2, + ref: 'main', target_sha: 'xxx') + build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2) + + expect { post_graphql(query, **args2) }.not_to exceed_all_query_limit(control) + + expect(graphql_data.count).to eq 1 + expect(graphql_data).to match( + a_hash_including( + 'runner' => a_graphql_entity_for( + project_runner2, + jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) } + ) + )) + end + end end end describe 'sorting and pagination' do let(:query) do <<~GQL - query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { - runner(id: $id) { - #{fields} + query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { + runner(id: $id) { + #{fields} + } } - } GQL end @@ -824,18 +970,18 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let(:fields) do <<~QUERY - projects(search: $projectSearchTerm, first: $n, after: $cursor) { - count - nodes { - id - } - pageInfo { - hasPreviousPage - startCursor - endCursor - hasNextPage + projects(search: $projectSearchTerm, first: $n, after: $cursor) { + count + nodes { + id + } + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } } - } QUERY end |