Commit 06b3aac8 authored by Pedro Pombeiro's avatar Pedro Pombeiro Committed by Matthias Käppler

Add runners GraphQL query

parent 41797d3f
# frozen_string_literal: true
module Resolvers
module Ci
class RunnersResolver < BaseResolver
type Types::Ci::RunnerType.connection_type, null: true
argument :status, ::Types::Ci::RunnerStatusEnum,
required: false,
description: 'Filter runners by status.'
argument :type, ::Types::Ci::RunnerTypeEnum,
required: false,
description: 'Filter runners by type.'
argument :tag_list, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter by tags associated with the runner (comma-separated or array).'
argument :sort, ::Types::Ci::RunnerSortEnum,
required: false,
description: 'Sort order of results.'
def resolve(**args)
::Ci::RunnersFinder
.new(current_user: current_user, params: runners_finder_params(args))
.execute
end
private
def runners_finder_params(params)
{
status_status: params[:status]&.to_s,
type_type: params[:type],
tag_name: params[:tag_list],
search: params[:search],
sort: params[:sort]&.to_s
}.compact
end
end
end
end
# frozen_string_literal: true
module Types
module Ci
class RunnerSortEnum < BaseEnum
graphql_name 'CiRunnerSort'
description 'Values for sorting runners'
value 'CONTACTED_ASC', 'Ordered by contacted_at in ascending order.', value: :contacted_asc
value 'CREATED_DESC', 'Ordered by created_date in descending order.', value: :created_date
end
end
end
...@@ -119,6 +119,12 @@ module Types ...@@ -119,6 +119,12 @@ module Types
description: "Find a runner.", description: "Find a runner.",
feature_flag: :runner_graphql_query feature_flag: :runner_graphql_query
field :runners, Types::Ci::RunnerType.connection_type,
null: true,
resolver: Resolvers::Ci::RunnersResolver,
description: "Find runners visible to the current user.",
feature_flag: :runner_graphql_query
field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1 field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1
def design_management def design_management
......
...@@ -322,6 +322,25 @@ Returns [`RunnerSetup`](#runnersetup). ...@@ -322,6 +322,25 @@ Returns [`RunnerSetup`](#runnersetup).
| <a id="queryrunnersetupplatform"></a>`platform` | [`String!`](#string) | Platform to generate the instructions for. | | <a id="queryrunnersetupplatform"></a>`platform` | [`String!`](#string) | Platform to generate the instructions for. |
| <a id="queryrunnersetupprojectid"></a>`projectId` **{warning-solid}** | [`ProjectID`](#projectid) | **Deprecated** in 13.11. No longer used. | | <a id="queryrunnersetupprojectid"></a>`projectId` **{warning-solid}** | [`ProjectID`](#projectid) | **Deprecated** in 13.11. No longer used. |
### `Query.runners`
Find runners visible to the current user. Available only when feature flag `runner_graphql_query` is enabled.
Returns [`CiRunnerConnection`](#cirunnerconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
| <a id="queryrunnersstatus"></a>`status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. |
| <a id="queryrunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
| <a id="queryrunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
### `Query.snippets` ### `Query.snippets`
Find Snippets visible to the current user. Find Snippets visible to the current user.
...@@ -4600,6 +4619,29 @@ The edge type for [`CiJob`](#cijob). ...@@ -4600,6 +4619,29 @@ The edge type for [`CiJob`](#cijob).
| <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. | | <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. |
#### `CiRunnerConnection`
The connection type for [`CiRunner`](#cirunner).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnerconnectionedges"></a>`edges` | [`[CiRunnerEdge]`](#cirunneredge) | A list of edges. |
| <a id="cirunnerconnectionnodes"></a>`nodes` | [`[CiRunner]`](#cirunner) | A list of nodes. |
| <a id="cirunnerconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `CiRunnerEdge`
The edge type for [`CiRunner`](#cirunner).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunneredgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="cirunneredgenode"></a>`node` | [`CiRunner`](#cirunner) | The item at the end of the edge. |
#### `CiStageConnection` #### `CiStageConnection`
The connection type for [`CiStage`](#cistage). The connection type for [`CiStage`](#cistage).
...@@ -13647,6 +13689,15 @@ Values for YAML processor result. ...@@ -13647,6 +13689,15 @@ Values for YAML processor result.
| <a id="cirunneraccesslevelnot_protected"></a>`NOT_PROTECTED` | A runner that is not protected. | | <a id="cirunneraccesslevelnot_protected"></a>`NOT_PROTECTED` | A runner that is not protected. |
| <a id="cirunneraccesslevelref_protected"></a>`REF_PROTECTED` | A runner that is ref protected. | | <a id="cirunneraccesslevelref_protected"></a>`REF_PROTECTED` | A runner that is ref protected. |
### `CiRunnerSort`
Values for sorting runners.
| Value | Description |
| ----- | ----------- |
| <a id="cirunnersortcontacted_asc"></a>`CONTACTED_ASC` | Ordered by contacted_at in ascending order. |
| <a id="cirunnersortcreated_desc"></a>`CREATED_DESC` | Ordered by created_date in descending order. |
### `CiRunnerStatus` ### `CiRunnerStatus`
| Value | Description | | Value | Description |
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnersResolver do
include GraphqlHelpers
let_it_be(:user) { create_default(:user, :admin) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:inactive_project_runner) do
create(:ci_runner, :project, projects: [project], active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
end
let_it_be(:offline_project_runner) do
create(:ci_runner, :project, projects: [project], contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
end
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], contacted_at: 1.second.ago) }
let_it_be(:instance_runner) { create(:ci_runner, :instance, contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
describe '#resolve' do
subject { resolve(described_class, ctx: { current_user: user }, args: args).items.to_a }
let(:args) do
{}
end
context 'without sort' do
it 'returns all the runners' do
is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner)
end
end
context 'with a sort argument' do
context "set to :contacted_asc" do
let(:args) do
{ sort: :contacted_asc }
end
it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner]) }
end
context "set to :created_date" do
let(:args) do
{ sort: :created_date }
end
it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner]) }
end
end
context 'when type is filtered' do
let(:args) do
{ type: runner_type.to_s }
end
context 'to instance runners' do
let(:runner_type) { :instance_type }
it 'returns the instance runner' do
is_expected.to contain_exactly(instance_runner)
end
end
context 'to group runners' do
let(:runner_type) { :group_type }
it 'returns the group runner' do
is_expected.to contain_exactly(group_runner)
end
end
context 'to project runners' do
let(:runner_type) { :project_type }
it 'returns the project runner' do
is_expected.to contain_exactly(inactive_project_runner, offline_project_runner)
end
end
end
context 'when status is filtered' do
let(:args) do
{ status: runner_status.to_s }
end
context 'to active runners' do
let(:runner_status) { :active }
it 'returns the instance and group runners' do
is_expected.to contain_exactly(offline_project_runner, group_runner, instance_runner)
end
end
context 'to offline runners' do
let(:runner_status) { :offline }
it 'returns the offline project runner' do
is_expected.to contain_exactly(offline_project_runner)
end
end
end
context 'when tag list is filtered' do
let(:args) do
{ tag_list: tag_list }
end
context 'with "project_runner" tag' do
let(:tag_list) { ['project_runner'] }
it 'returns the project_runner runners' do
is_expected.to contain_exactly(offline_project_runner, inactive_project_runner)
end
end
context 'with "project_runner" and "active_runner" tags as comma-separated string' do
let(:tag_list) { ['project_runner,active_runner'] }
it 'returns the offline_project_runner runner' do
is_expected.to contain_exactly(offline_project_runner)
end
end
context 'with "active_runner" and "instance_runner" tags as array' do
let(:tag_list) { %w[instance_runner active_runner] }
it 'returns the offline_project_runner runner' do
is_expected.to contain_exactly(instance_runner)
end
end
end
end
end
...@@ -25,6 +25,7 @@ RSpec.describe GitlabSchema.types['Query'] do ...@@ -25,6 +25,7 @@ RSpec.describe GitlabSchema.types['Query'] do
usage_trends_measurements usage_trends_measurements
runner_platforms runner_platforms
runner runner
runners
] ]
expect(described_class).to have_graphql_fields(*expected_fields).at_least expect(described_class).to have_graphql_fields(*expected_fields).at_least
...@@ -91,6 +92,12 @@ RSpec.describe GitlabSchema.types['Query'] do ...@@ -91,6 +92,12 @@ RSpec.describe GitlabSchema.types['Query'] do
it { is_expected.to have_graphql_type(Types::Ci::RunnerType) } it { is_expected.to have_graphql_type(Types::Ci::RunnerType) }
end end
describe 'runners field' do
subject { described_class.fields['runners'] }
it { is_expected.to have_graphql_type(Types::Ci::RunnerType.connection_type) }
end
describe 'runner_platforms field' do describe 'runner_platforms field' do
subject { described_class.fields['runnerPlatforms'] } subject { described_class.fields['runnerPlatforms'] }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.runners' do
include GraphqlHelpers
let_it_be(:current_user) { create_default(:user, :admin) }
describe 'Query.runners' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: 'abc', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, active: false, version: 'def', revision: '456', description: 'Project runner', projects: [project], ip_address: '127.0.0.1') }
let(:runners_graphql_data) { graphql_data['runners'] }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('CiRunner')}
}
QUERY
end
let(:query) do
%(
query {
runners(type:#{runner_type},status:#{status}) {
#{fields}
}
}
)
end
before do
post_graphql(query, current_user: current_user)
end
shared_examples 'a working graphql query returning expected runner' do
it_behaves_like 'a working graphql query'
it 'returns expected runner' do
expect(runners_graphql_data['nodes'].map { |n| n['id'] }).to contain_exactly(expected_runner.to_global_id.to_s)
end
end
context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
let(:runner_type) { 'INSTANCE_TYPE' }
let(:status) { 'ACTIVE' }
let!(:expected_runner) { instance_runner }
it_behaves_like 'a working graphql query returning expected runner'
end
context 'runner_type is PROJECT_TYPE and status is NOT_CONNECTED' do
let(:runner_type) { 'PROJECT_TYPE' }
let(:status) { 'NOT_CONNECTED' }
let!(:expected_runner) { project_runner }
it_behaves_like 'a working graphql query returning expected runner'
end
end
describe 'pagination' do
let(:data_path) { [:runners] }
def pagination_query(params)
graphql_query_for(:runners, params, "#{page_info} nodes { id }")
end
def pagination_results_data(runners)
runners.map { |runner| GitlabSchema.parse_gid(runner['id'], expected_type: ::Ci::Runner).model_id.to_i }
end
let_it_be(:runners) do
common_args = {
version: 'abc',
revision: '123',
ip_address: '127.0.0.1'
}
[
create(:ci_runner, :instance, created_at: 4.days.ago, contacted_at: 3.days.ago, **common_args),
create(:ci_runner, :instance, created_at: 30.hours.ago, contacted_at: 1.day.ago, **common_args),
create(:ci_runner, :instance, created_at: 1.day.ago, contacted_at: 1.hour.ago, **common_args),
create(:ci_runner, :instance, created_at: 2.days.ago, contacted_at: 2.days.ago, **common_args),
create(:ci_runner, :instance, created_at: 3.days.ago, contacted_at: 1.second.ago, **common_args)
]
end
context 'when sorted by contacted_at ascending' do
let(:ordered_runners) { runners.sort_by(&:contacted_at) }
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :CONTACTED_ASC }
let(:first_param) { 2 }
let(:expected_results) { ordered_runners.map(&:id) }
end
end
context 'when sorted by created_at' do
let(:ordered_runners) { runners.sort_by(&:created_at).reverse }
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :CREATED_DESC }
let(:first_param) { 2 }
let(:expected_results) { ordered_runners.map(&:id) }
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment