Commit 6adff1dd authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'pedropombeiro/342799/add-stale-runner-graphql-query' into 'master'

GraphQL: Add STALE value to CiRunnerStatus enum

See merge request gitlab-org/gitlab!74619
parents 015aa1fc be13333b
# frozen_string_literal: true
module Resolvers
module Ci
# NOTE: This class was introduced to allow modifying the meaning of certain values in RunnerStatusEnum
# while preserving backward compatibility. It can be removed in 15.0 once the API has stabilized.
class RunnerStatusResolver < BaseResolver
type Types::Ci::RunnerStatusEnum, null: false
alias_method :runner, :object
argument :legacy_mode,
type: GraphQL::Types::String,
default_value: '14.5',
required: false,
description: 'Compatibility mode. A null value turns off compatibility mode.',
deprecated: { reason: 'Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null', milestone: '14.6' }
def resolve(legacy_mode:, **args)
runner.status(legacy_mode)
end
end
end
end
...@@ -5,24 +5,33 @@ module Types ...@@ -5,24 +5,33 @@ module Types
class RunnerStatusEnum < BaseEnum class RunnerStatusEnum < BaseEnum
graphql_name 'CiRunnerStatus' graphql_name 'CiRunnerStatus'
::Ci::Runner::AVAILABLE_STATUSES.each do |status| value 'ACTIVE',
description = case status description: 'Runner that is not paused.',
when 'active' deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
"A runner that is not paused." value: :active
when 'online'
"A runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
when 'offline'
"A runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
when 'not_connected'
"A runner that has never contacted this instance."
else
"A runner that is #{status.to_s.tr('_', ' ')}."
end
value status.to_s.upcase, value 'PAUSED',
description: description, description: 'Runner that is paused.',
value: status.to_sym deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
end value: :paused
value 'ONLINE',
description: "Runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
value: :online
value 'OFFLINE',
description: "Runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline', milestone: '14.6' },
value: :offline
value 'STALE',
description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0",
value: :stale
value 'NOT_CONNECTED',
description: 'Runner that has never contacted this instance.',
deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact', milestone: '14.6' },
value: :not_connected
end end
end end
end end
...@@ -27,8 +27,11 @@ module Types ...@@ -27,8 +27,11 @@ module Types
description: 'Access level of the runner.' description: 'Access level of the runner.'
field :active, GraphQL::Types::Boolean, null: false, field :active, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is allowed to receive jobs.' description: 'Indicates the runner is allowed to receive jobs.'
field :status, ::Types::Ci::RunnerStatusEnum, null: false, field :status,
description: 'Status of the runner.' Types::Ci::RunnerStatusEnum,
null: false,
description: 'Status of the runner.',
resolver: ::Resolvers::Ci::RunnerStatusResolver
field :version, GraphQL::Types::String, null: true, field :version, GraphQL::Types::String, null: true,
description: 'Version of the runner.' description: 'Version of the runner.'
field :short_sha, GraphQL::Types::String, null: true, field :short_sha, GraphQL::Types::String, null: true,
...@@ -50,7 +53,7 @@ module Types ...@@ -50,7 +53,7 @@ module Types
field :job_count, GraphQL::Types::Int, null: true, field :job_count, GraphQL::Types::Int, null: true,
description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)." description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)."
field :admin_url, GraphQL::Types::String, null: true, field :admin_url, GraphQL::Types::String, null: true,
description: 'Admin URL of the runner. Only available for adminstrators.' description: 'Admin URL of the runner. Only available for administrators.'
def job_count def job_count
# We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT
......
...@@ -44,7 +44,7 @@ module Ci ...@@ -44,7 +44,7 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze AVAILABLE_TYPES = runner_types.keys.freeze
AVAILABLE_STATUSES = %w[active paused online offline not_connected].freeze AVAILABLE_STATUSES = %w[active paused online offline not_connected stale].freeze
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
...@@ -287,10 +287,15 @@ module Ci ...@@ -287,10 +287,15 @@ module Ci
end end
def stale? def stale?
return false unless created_at
[created_at, contacted_at].compact.max < self.class.stale_deadline [created_at, contacted_at].compact.max < self.class.stale_deadline
end end
def status def status(legacy_mode = nil)
return deprecated_rest_status if legacy_mode == '14.5'
return :stale if stale?
return :not_connected unless contacted_at return :not_connected unless contacted_at
online? ? :online : :offline online? ? :online : :offline
......
...@@ -8750,7 +8750,7 @@ Represents the total number of issues and their weights for a particular day. ...@@ -8750,7 +8750,7 @@ Represents the total number of issues and their weights for a particular day.
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="cirunneraccesslevel"></a>`accessLevel` | [`CiRunnerAccessLevel!`](#cirunneraccesslevel) | Access level of the runner. | | <a id="cirunneraccesslevel"></a>`accessLevel` | [`CiRunnerAccessLevel!`](#cirunneraccesslevel) | Access level of the runner. |
| <a id="cirunneractive"></a>`active` | [`Boolean!`](#boolean) | Indicates the runner is allowed to receive jobs. | | <a id="cirunneractive"></a>`active` | [`Boolean!`](#boolean) | Indicates the runner is allowed to receive jobs. |
| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for adminstrators. | | <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for administrators. |
| <a id="cirunnercontactedat"></a>`contactedAt` | [`Time`](#time) | Last contact from the runner. | | <a id="cirunnercontactedat"></a>`contactedAt` | [`Time`](#time) | Last contact from the runner. |
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. | | <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. | | <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
...@@ -8765,11 +8765,24 @@ Represents the total number of issues and their weights for a particular day. ...@@ -8765,11 +8765,24 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. | | <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. |
| <a id="cirunnerrunnertype"></a>`runnerType` | [`CiRunnerType!`](#cirunnertype) | Type of the runner. | | <a id="cirunnerrunnertype"></a>`runnerType` | [`CiRunnerType!`](#cirunnertype) | Type of the runner. |
| <a id="cirunnershortsha"></a>`shortSha` | [`String`](#string) | First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID. | | <a id="cirunnershortsha"></a>`shortSha` | [`String`](#string) | First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID. |
| <a id="cirunnerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner. |
| <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. | | <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. |
| <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. | | <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. |
| <a id="cirunnerversion"></a>`version` | [`String`](#string) | Version of the runner. | | <a id="cirunnerversion"></a>`version` | [`String`](#string) | Version of the runner. |
#### Fields with arguments
##### `CiRunner.status`
Status of the runner.
Returns [`CiRunnerStatus!`](#cirunnerstatus).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnerstatuslegacymode"></a>`legacyMode` **{warning-solid}** | [`String`](#string) | **Deprecated** in 14.6. Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null. |
### `CiStage` ### `CiStage`
#### Fields #### Fields
...@@ -15956,11 +15969,12 @@ Values for sorting runners. ...@@ -15956,11 +15969,12 @@ Values for sorting runners.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| <a id="cirunnerstatusactive"></a>`ACTIVE` | A runner that is not paused. | | <a id="cirunnerstatusactive"></a>`ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` | A runner that has never contacted this instance. | | <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact. |
| <a id="cirunnerstatusoffline"></a>`OFFLINE` | A runner that has not contacted this instance within the last 2 hours. | | <a id="cirunnerstatusoffline"></a>`OFFLINE` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline. |
| <a id="cirunnerstatusonline"></a>`ONLINE` | A runner that contacted this instance within the last 2 hours. | | <a id="cirunnerstatusonline"></a>`ONLINE` | Runner that contacted this instance within the last 2 hours. |
| <a id="cirunnerstatuspaused"></a>`PAUSED` | A runner that is paused. | | <a id="cirunnerstatuspaused"></a>`PAUSED` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
| <a id="cirunnerstatusstale"></a>`STALE` | Runner that has not contacted this instance within the last 3 months. Only available if legacyMode is null. Will be a possible return value starting in 15.0. |
### `CiRunnerType` ### `CiRunnerType`
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnerStatusResolver do
include GraphqlHelpers
describe '#resolve' do
let(:user) { build(:user) }
let(:runner) { build(:ci_runner) }
subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, obj: runner, args: args) }
context 'with legacy_mode' do
context 'set to 14.5' do
let(:args) do
{ legacy_mode: '14.5' }
end
it 'calls runner.status with specified legacy_mode' do
expect(runner).to receive(:status).with('14.5').once.and_return(:online)
expect(resolve_subject).to eq(:online)
end
end
context 'set to nil' do
let(:args) do
{ legacy_mode: nil }
end
it 'calls runner.status with specified legacy_mode' do
expect(runner).to receive(:status).with(nil).once.and_return(:stale)
expect(resolve_subject).to eq(:stale)
end
end
end
end
end
...@@ -342,6 +342,7 @@ RSpec.describe Ci::Runner do ...@@ -342,6 +342,7 @@ RSpec.describe Ci::Runner do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:created_at, :contacted_at, :expected_stale?) do where(:created_at, :contacted_at, :expected_stale?) do
nil | nil | false
3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true 3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true
3.months.ago - 1.second | 3.months.ago + 1.hour | false 3.months.ago - 1.second | 3.months.ago + 1.hour | false
3.months.ago - 1.second | nil | true 3.months.ago - 1.second | nil | true
...@@ -376,6 +377,8 @@ RSpec.describe Ci::Runner do ...@@ -376,6 +377,8 @@ RSpec.describe Ci::Runner do
end end
def stub_redis_runner_contacted_at(value) def stub_redis_runner_contacted_at(value)
return unless created_at
Gitlab::Redis::Cache.with do |redis| Gitlab::Redis::Cache.with do |redis|
cache_key = runner.send(:cache_attribute_key) cache_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:get).with(cache_key) expect(redis).to receive(:get).with(cache_key)
...@@ -419,7 +422,7 @@ RSpec.describe Ci::Runner do ...@@ -419,7 +422,7 @@ RSpec.describe Ci::Runner do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'contacted long time ago time' do context 'contacted long time ago' do
before do before do
runner.contacted_at = 1.year.ago runner.contacted_at = 1.year.ago
end end
...@@ -437,7 +440,7 @@ RSpec.describe Ci::Runner do ...@@ -437,7 +440,7 @@ RSpec.describe Ci::Runner do
end end
context 'with cache value' do context 'with cache value' do
context 'contacted long time ago time' do context 'contacted long time ago' do
before do before do
runner.contacted_at = 1.year.ago runner.contacted_at = 1.year.ago
stub_redis_runner_contacted_at(1.year.ago.to_s) stub_redis_runner_contacted_at(1.year.ago.to_s)
...@@ -699,16 +702,33 @@ RSpec.describe Ci::Runner do ...@@ -699,16 +702,33 @@ RSpec.describe Ci::Runner do
end end
describe '#status' do describe '#status' do
let(:runner) { build(:ci_runner, :instance) } let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) }
let(:legacy_mode) { }
subject { runner.status } subject { runner.status(legacy_mode) }
context 'never connected' do context 'never connected' do
before do before do
runner.contacted_at = nil runner.contacted_at = nil
end end
it { is_expected.to eq(:not_connected) } context 'with legacy_mode enabled' do
let(:legacy_mode) { '14.5' }
it { is_expected.to eq(:not_connected) }
end
context 'with legacy_mode disabled' do
it { is_expected.to eq(:stale) }
end
context 'created recently' do
before do
runner.created_at = 1.day.ago
end
it { is_expected.to eq(:not_connected) }
end
end end
context 'inactive but online' do context 'inactive but online' do
...@@ -717,7 +737,15 @@ RSpec.describe Ci::Runner do ...@@ -717,7 +737,15 @@ RSpec.describe Ci::Runner do
runner.active = false runner.active = false
end end
it { is_expected.to eq(:online) } context 'with legacy_mode enabled' do
let(:legacy_mode) { '14.5' }
it { is_expected.to eq(:paused) }
end
context 'with legacy_mode disabled' do
it { is_expected.to eq(:online) }
end
end end
context 'contacted 1s ago' do context 'contacted 1s ago' do
...@@ -728,13 +756,29 @@ RSpec.describe Ci::Runner do ...@@ -728,13 +756,29 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(:online) } it { is_expected.to eq(:online) }
end end
context 'contacted long time ago' do context 'contacted recently' do
before do before do
runner.contacted_at = 1.year.ago runner.contacted_at = (3.months - 1.hour).ago
end end
it { is_expected.to eq(:offline) } it { is_expected.to eq(:offline) }
end end
context 'contacted long time ago' do
before do
runner.contacted_at = (3.months + 1.second).ago
end
context 'with legacy_mode enabled' do
let(:legacy_mode) { '14.5' }
it { is_expected.to eq(:offline) }
end
context 'with legacy_mode disabled' do
it { is_expected.to eq(:stale) }
end
end
end end
describe '#deprecated_rest_status' do describe '#deprecated_rest_status' do
......
...@@ -63,7 +63,7 @@ RSpec.describe 'Query.runner(id)' do ...@@ -63,7 +63,7 @@ RSpec.describe 'Query.runner(id)' do
'revision' => runner.revision, 'revision' => runner.revision,
'locked' => false, 'locked' => false,
'active' => runner.active, 'active' => runner.active,
'status' => runner.status.to_s.upcase, 'status' => runner.status('14.5').to_s.upcase,
'maximumTimeout' => runner.maximum_timeout, 'maximumTimeout' => runner.maximum_timeout,
'accessLevel' => runner.access_level.to_s.upcase, 'accessLevel' => runner.access_level.to_s.upcase,
'runUntagged' => runner.run_untagged, 'runUntagged' => runner.run_untagged,
...@@ -221,6 +221,45 @@ RSpec.describe 'Query.runner(id)' do ...@@ -221,6 +221,45 @@ RSpec.describe 'Query.runner(id)' do
end 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(:query) do
%(
query {
staleRunner: runner(id: "#{stale_runner.to_global_id}") {
status
legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
newStatus: status(legacyMode: null)
}
pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") {
status
legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
newStatus: status(legacyMode: null)
}
}
)
end
it 'retrieves status fields with expected values' do
post_graphql(query, current_user: user)
stale_runner_data = graphql_data_at(:stale_runner)
expect(stale_runner_data).to match a_hash_including(
'status' => 'NOT_CONNECTED',
'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED',
'newStatus' => 'STALE'
)
paused_runner_data = graphql_data_at(:paused_runner)
expect(paused_runner_data).to match a_hash_including(
'status' => 'PAUSED',
'legacyStatusWithExplicitVersion' => 'PAUSED',
'newStatus' => 'OFFLINE'
)
end
end
describe 'for multiple runners' do describe 'for multiple runners' do
let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project1) { create(:project, :test_repo) }
let_it_be(:project2) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) }
......
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