From 8caeaa1e9a922acb719453866d93e1180a6e2dbb Mon Sep 17 00:00:00 2001
From: Vladimir Shushlin <vshushlin@gitlab.com>
Date: Fri, 2 Jun 2023 13:32:24 +0000
Subject: [PATCH] Add upgrade status to RunnerManager in GraphQL

Changelog: added
EE: true
---
 app/graphql/types/ci/runner_manager_type.rb   |  2 +
 doc/api/graphql/reference/index.md            |  1 +
 .../ee/types/ci/runner_manager_type.rb        | 38 +++++++++++++++++++
 ee/app/graphql/ee/types/ci/runner_type.rb     |  4 +-
 ee/app/policies/ee/global_policy.rb           | 12 ++++++
 .../types/ci/runner_manager_type_spec.rb      | 13 +++++++
 ee/spec/policies/global_policy_spec.rb        | 22 +++++++++++
 .../requests/api/graphql/ci/runner_spec.rb    | 13 ++++++-
 .../types/ci/runner_manager_type_spec.rb      |  2 +-
 spec/requests/api/graphql/ci/jobs_spec.rb     |  4 +-
 10 files changed, 104 insertions(+), 7 deletions(-)
 create mode 100644 ee/app/graphql/ee/types/ci/runner_manager_type.rb
 create mode 100644 ee/spec/graphql/types/ci/runner_manager_type_spec.rb

diff --git a/app/graphql/types/ci/runner_manager_type.rb b/app/graphql/types/ci/runner_manager_type.rb
index 2a5053f8f0745..9c89b6537ea27 100644
--- a/app/graphql/types/ci/runner_manager_type.rb
+++ b/app/graphql/types/ci/runner_manager_type.rb
@@ -47,3 +47,5 @@ def executor_name
     end
   end
 end
+
+Types::Ci::RunnerManagerType.prepend_mod_with('Types::Ci::RunnerManagerType')
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 810dae24f2509..9f6baa5495b42 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12941,6 +12941,7 @@ Returns [`CiRunnerStatus!`](#cirunnerstatus).
 | <a id="cirunnermanagerrunner"></a>`runner` | [`CiRunner`](#cirunner) | Runner configuration for the runner manager. |
 | <a id="cirunnermanagerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner manager. |
 | <a id="cirunnermanagersystemid"></a>`systemId` | [`String!`](#string) | System ID associated with the runner manager. |
+| <a id="cirunnermanagerupgradestatus"></a>`upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Availability of upgrades for the runner manager. |
 | <a id="cirunnermanagerversion"></a>`version` | [`String`](#string) | Version of the runner. |
 
 ### `CiSecureFileRegistry`
diff --git a/ee/app/graphql/ee/types/ci/runner_manager_type.rb b/ee/app/graphql/ee/types/ci/runner_manager_type.rb
new file mode 100644
index 0000000000000..56e2b8248c17b
--- /dev/null
+++ b/ee/app/graphql/ee/types/ci/runner_manager_type.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module EE
+  module Types
+    module Ci
+      module RunnerManagerType
+        extend ActiveSupport::Concern
+
+        RUNNER_UPGRADE_STATUS_TRANSLATIONS = {
+          error: nil
+        }.freeze
+
+        prepended do
+          field :upgrade_status, ::Types::Ci::RunnerUpgradeStatusEnum,
+            null: true,
+            description: 'Availability of upgrades for the runner manager.',
+            alpha: { milestone: '16.1' }
+
+          def upgrade_status
+            return unless upgrade_status_available?
+
+            _, status = ::Gitlab::Ci::RunnerUpgradeCheck.new(::Gitlab::VERSION)
+              .check_runner_upgrade_suggestion(runner_manager.version)
+            RUNNER_UPGRADE_STATUS_TRANSLATIONS.fetch(status, status)
+          end
+
+          private
+
+          def upgrade_status_available?
+            return false unless ::Gitlab::Ci::RunnerReleases.instance.enabled?
+
+            Ability.allowed?(current_user, :read_runner_upgrade_status)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/ee/types/ci/runner_type.rb b/ee/app/graphql/ee/types/ci/runner_type.rb
index 0e8ccb83c5de2..00f7627090b6f 100644
--- a/ee/app/graphql/ee/types/ci/runner_type.rb
+++ b/ee/app/graphql/ee/types/ci/runner_type.rb
@@ -19,6 +19,8 @@ module RunnerType
             null: true,
             description: 'Private projects\' "minutes cost factor" associated with the runner (GitLab.com only).'
 
+          # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/411945
+          # this should take the maximum status from runner managers
           field :upgrade_status, ::Types::Ci::RunnerUpgradeStatusEnum,
             null: true,
             description: 'Availability of upgrades for the runner.',
@@ -37,7 +39,7 @@ def upgrade_status
           def upgrade_status_available?
             return false unless ::Gitlab::Ci::RunnerReleases.instance.enabled?
 
-            License.feature_available?(:runner_upgrade_management) || current_user&.has_paid_namespace?
+            Ability.allowed?(current_user, :read_runner_upgrade_status)
           end
         end
       end
diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb
index dedf427637cd5..0535ecbd916ba 100644
--- a/ee/app/policies/ee/global_policy.rb
+++ b/ee/app/policies/ee/global_policy.rb
@@ -41,6 +41,10 @@ module GlobalPolicy
         ::License.feature_available?(:runner_jobs_statistics)
       end
 
+      condition(:runner_upgrade_management_available) do
+        License.feature_available?(:runner_upgrade_management)
+      end
+
       condition(:service_accounts_available) do
         ::License.feature_available?(:service_accounts)
       end
@@ -61,6 +65,12 @@ module GlobalPolicy
         accessible_root_groups.reject(&:code_suggestions_enabled?).any?
       end
 
+      condition(:user_has_paid_namespace) do
+        next false unless @user
+
+        @user.has_paid_namespace?
+      end
+
       rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
 
       rule { ~anonymous & remote_development_available }.enable :read_workspace
@@ -106,6 +116,8 @@ module GlobalPolicy
 
       rule { code_suggestions_enabled }.enable :access_code_suggestions
       rule { code_suggestions_disabled_by_group }.prevent :access_code_suggestions
+
+      rule { runner_upgrade_management_available | user_has_paid_namespace }.enable :read_runner_upgrade_status
     end
   end
 end
diff --git a/ee/spec/graphql/types/ci/runner_manager_type_spec.rb b/ee/spec/graphql/types/ci/runner_manager_type_spec.rb
new file mode 100644
index 0000000000000..58d176e86dd30
--- /dev/null
+++ b/ee/spec/graphql/types/ci/runner_manager_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiRunnerManager'], feature_category: :runner_fleet do
+  it { expect(described_class.graphql_name).to eq('CiRunnerManager') }
+
+  it 'includes the ee specific fields' do
+    expected_fields = %w[upgrade_status]
+
+    expect(described_class).to include_graphql_fields(*expected_fields)
+  end
+end
diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb
index e93e125783147..05af1ae977dbc 100644
--- a/ee/spec/policies/global_policy_spec.rb
+++ b/ee/spec/policies/global_policy_spec.rb
@@ -396,6 +396,28 @@
     end
   end
 
+  describe 'read_runner_upgrade_status' do
+    it { is_expected.to be_disallowed(:read_runner_upgrade_status) }
+
+    context 'when runner_upgrade_management is available' do
+      before do
+        stub_licensed_features(runner_upgrade_management: true)
+      end
+
+      it { is_expected.to be_allowed(:read_runner_upgrade_status) }
+    end
+
+    context 'when user has paid namespace' do
+      before do
+        allow(Gitlab).to receive(:com?).and_return true
+        group = create(:group_with_plan, plan: :ultimate_plan)
+        group.add_maintainer(user)
+      end
+
+      it { expect(described_class.new(user, nil)).to be_allowed(:read_runner_upgrade_status) }
+    end
+  end
+
   describe 'admin_service_accounts' do
     subject { described_class.new(admin, [user]) }
 
diff --git a/ee/spec/requests/api/graphql/ci/runner_spec.rb b/ee/spec/requests/api/graphql/ci/runner_spec.rb
index 8dae521af7346..bb63c079e6957 100644
--- a/ee/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/ee/spec/requests/api/graphql/ci/runner_spec.rb
@@ -10,7 +10,14 @@
 
   shared_examples 'runner details fetch operation returning expected upgradeStatus' do
     let(:query) do
-      wrap_fields(query_graphql_path(query_path, 'id upgradeStatus'))
+      managers = <<~GRAPHQL
+        managers{
+          nodes {
+            upgradeStatus
+          }
+        }
+      GRAPHQL
+      wrap_fields(query_graphql_path(query_path, "id upgradeStatus #{managers}"))
     end
 
     let(:query_path) do
@@ -23,7 +30,7 @@
       allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
         allow(instance).to receive(:check_runner_upgrade_suggestion)
           .and_return([nil, upgrade_status])
-          .once
+          .twice # once for the runner and another for the runner manager
       end
     end
 
@@ -34,6 +41,7 @@
 
       expect(runner_data).not_to be_nil
       expect(runner_data).to match a_graphql_entity_for(runner, upgrade_status: expected_upgrade_status)
+      expect(graphql_dig_at(runner_data, :managers, :nodes, 0, :upgrade_status)).to eq(expected_upgrade_status)
     end
 
     context 'when fetching runner releases is disabled' do
@@ -54,6 +62,7 @@
 
   describe 'upgradeStatus', :saas do
     let_it_be(:runner) { create(:ci_runner, description: 'Runner 1', version: '14.1.0', revision: 'a') }
+    let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner, version: '14.1.0', revision: 'a') }
 
     context 'requested by non-paid user' do
       let(:current_user) { admin }
diff --git a/spec/graphql/types/ci/runner_manager_type_spec.rb b/spec/graphql/types/ci/runner_manager_type_spec.rb
index 240e1edbf78bc..6f73171cd8f88 100644
--- a/spec/graphql/types/ci/runner_manager_type_spec.rb
+++ b/spec/graphql/types/ci/runner_manager_type_spec.rb
@@ -13,6 +13,6 @@
       runner status system_id version
     ]
 
-    expect(described_class).to have_graphql_fields(*expected_fields)
+    expect(described_class).to include_graphql_fields(*expected_fields)
   end
 end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index f237516021dbe..756fcd8b7cded 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -433,8 +433,6 @@ def all(*fields)
     end
 
     it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
-      admin2 = create(:admin)
-
       control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
         post_graphql(query, current_user: admin)
       end
@@ -442,7 +440,7 @@ def all(*fields)
       runner_manager2 = create(:ci_runner_machine)
       create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2)
 
-      expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control)
+      expect { post_graphql(query, current_user: admin) }.not_to exceed_all_query_limit(control)
     end
   end
 
-- 
GitLab