From 6f20fc2b0ef7dde5822347ae910d266bed06e25d Mon Sep 17 00:00:00 2001
From: Chou Yu Ta <lawrencechou1024@gmail.com>
Date: Sat, 21 Dec 2024 13:51:54 +0000
Subject: [PATCH] Revert "Rename `ServerInfoPresenter` to
 `Gitlab::Kas::ServerInfo`"

This reverts commit 45c0eb6f93a4e54c0e5f15c38f6a699c75ddc21c.
---
 app/controllers/admin/dashboard_controller.rb |  2 +-
 app/models/app_config/kas_metadata.rb         |  2 +-
 .../gitlab/kas/server_info_presenter.rb       | 28 +-----
 doc/api/metadata.md                           |  2 +-
 lib/gitlab/kas/server_info.rb                 | 44 +++++++++
 spec/lib/gitlab/kas/server_info_spec.rb       | 95 +++++++++++++++++++
 spec/models/app_config/kas_metadata_spec.rb   |  7 +-
 .../gitlab/kas/server_info_presenter_spec.rb  | 81 ++++------------
 .../api/graphql/metadata_query_spec.rb        | 43 ++++++++-
 .../admin/dashboard/index.html.haml_spec.rb   | 11 ++-
 10 files changed, 215 insertions(+), 100 deletions(-)
 create mode 100644 lib/gitlab/kas/server_info.rb
 create mode 100644 spec/lib/gitlab/kas/server_info_spec.rb

diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index d657b1de1055..bad28c4f49cf 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -15,7 +15,7 @@ def index
     @users = User.order_id_desc.limit(10)
     @groups = Group.order_id_desc.with_route.limit(10)
     @notices = Gitlab::ConfigChecker::ExternalDatabaseChecker.check
-    @kas_server_info = Gitlab::Kas::ServerInfoPresenter.new if Gitlab::Kas.enabled?
+    @kas_server_info = Gitlab::Kas::ServerInfo.new.present if Gitlab::Kas.enabled?
     @redis_versions = Gitlab::Redis::ALL_CLASSES.map(&:version).uniq
   end
 
diff --git a/app/models/app_config/kas_metadata.rb b/app/models/app_config/kas_metadata.rb
index 917eefaaf9b9..496c1407b31d 100644
--- a/app/models/app_config/kas_metadata.rb
+++ b/app/models/app_config/kas_metadata.rb
@@ -10,7 +10,7 @@ def self.declarative_policy_class
 
     def initialize
       @enabled = Gitlab::Kas.enabled?
-      @version = Gitlab::Kas.version if @enabled
+      @version = Gitlab::Kas::ServerInfo.new.version_info if @enabled
       @external_url = Gitlab::Kas.external_url if @enabled
       @external_k8s_proxy_url = Gitlab::Kas.tunnel_url if @enabled
     end
diff --git a/app/presenters/gitlab/kas/server_info_presenter.rb b/app/presenters/gitlab/kas/server_info_presenter.rb
index 4096e705ae61..3aa114d4053f 100644
--- a/app/presenters/gitlab/kas/server_info_presenter.rb
+++ b/app/presenters/gitlab/kas/server_info_presenter.rb
@@ -2,45 +2,27 @@
 
 module Gitlab
   module Kas
-    class ServerInfoPresenter
+    class ServerInfoPresenter < Gitlab::View::Presenter::Delegated
+      presents ::Gitlab::Kas::ServerInfo, as: :server_info
+
       GROUP = 'gitlab-org/cluster-integration'
       PROJECT = 'gitlab-agent'
       private_constant :GROUP, :PROJECT
 
-      delegate :git_ref, to: :server_info, private: true
-      delegate :version, to: :server_info
-
-      def initialize
-        @server_info = fetch_server_info
-      end
-
-      def retrieved_server_info?
-        server_info.present?
-      end
-
       def git_ref_for_display
-        return unless git_ref.present?
+        return unless retrieved_server_info? && git_ref.present?
 
         commit&.short_id || git_ref
       end
 
       def git_ref_url
-        return unless git_ref.present?
+        return unless retrieved_server_info? && git_ref.present?
 
         build_git_ref_url
       end
 
       private
 
-      attr_reader :server_info
-
-      def fetch_server_info
-        Gitlab::Kas::Client.new.get_server_info
-      rescue StandardError => e
-        Gitlab::ErrorTracking.track_exception(e)
-        nil
-      end
-
       def build_git_ref_url
         Gitlab::Utils.append_path(Gitlab::Saas.com_url, git_ref_path)
       end
diff --git a/doc/api/metadata.md b/doc/api/metadata.md
index 6dfe6b7ab880..a144166696c7 100644
--- a/doc/api/metadata.md
+++ b/doc/api/metadata.md
@@ -30,7 +30,7 @@ Response body attributes:
 | `kas.enabled`             | boolean        | Indicates whether KAS is enabled.                                                                                             |
 | `kas.externalUrl`         | string or null | URL used by the agents to communicate with KAS. It's `null` if `kas.enabled` is `false`.                                      |
 | `kas.externalK8sProxyUrl` | string or null | URL used by the Kubernetes tooling to communicate with the KAS Kubernetes API proxy. It's `null` if `kas.enabled` is `false`. |
-| `kas.version`             | string or null | Version of KAS. It's `null` if `kas.enabled` is `false`.                                                                      |
+| `kas.version`             | string or null | Version of KAS. It's `null` if `kas.enabled` is `false` or when GitLab instance failed to fetch server info from KAS.         |
 | `enterprise`              | boolean        | Indicates whether GitLab instance is Enterprise Edition.                                                                      |
 
 Example request:
diff --git a/lib/gitlab/kas/server_info.rb b/lib/gitlab/kas/server_info.rb
new file mode 100644
index 000000000000..c2dd98f77404
--- /dev/null
+++ b/lib/gitlab/kas/server_info.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Kas
+    class ServerInfo
+      include Presentable
+
+      delegate :git_ref, :version, to: :fetched_server_info
+
+      def initialize
+        @fetched_server_info = fetch_server_info
+      end
+
+      def retrieved_server_info?
+        fetched_server_info.present?
+      end
+
+      def version_info
+        return unless retrieved_server_info? && version.present?
+        return Gitlab::VersionInfo.parse("#{version}+#{git_ref}", parse_suffix: true) if valid_commit_ref?
+
+        Gitlab::VersionInfo.parse(version)
+      end
+
+      def valid_commit_ref?
+        return unless retrieved_server_info?
+
+        ::Gitlab::Git::Commit.valid?(git_ref) &&
+          git_ref.match?(::Gitlab::Git::COMMIT_ID)
+      end
+
+      private
+
+      attr_reader :fetched_server_info
+
+      def fetch_server_info
+        Gitlab::Kas::Client.new.get_server_info
+      rescue StandardError => e
+        Gitlab::ErrorTracking.track_exception(e)
+        nil
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/kas/server_info_spec.rb b/spec/lib/gitlab/kas/server_info_spec.rb
new file mode 100644
index 000000000000..edfc4ac8d322
--- /dev/null
+++ b/spec/lib/gitlab/kas/server_info_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kas::ServerInfo, feature_category: :deployment_management do
+  subject(:server_info) { described_class.new }
+
+  let(:git_ref) { '6a0281c68969d9ce8f36fdaf242b4f6e0503d940' }
+
+  before do
+    allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+
+    # rubocop:disable RSpec/VerifiedDoubles -- Avoiding the false positive 'the Gitlab::Agent::ServerInfo::ServerInfo
+    # class does not implement the instance method: version' when using instance_double(), which is only because
+    # Ruby protobuf handle these message through method_missing instead of actually defining instance methods.
+    response = double(Gitlab::Agent::ServerInfo::ServerInfo, version: '17.4.0-rc1', git_ref: git_ref)
+    # rubocop:enable RSpec/VerifiedDoubles
+
+    allow_next_instance_of(Gitlab::Kas::Client) do |instance|
+      allow(instance).to receive(:get_server_info).and_return(response)
+    end
+  end
+
+  shared_examples 'logs kas error' do
+    it 'logs the error' do
+      expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error)
+
+      subject
+    end
+  end
+
+  context 'when kas client initialization fails' do
+    let(:error) { Gitlab::Kas::Client::ConfigurationError.new('boom') }
+
+    before do
+      allow(Gitlab::Kas::Client).to receive(:new).and_raise(error)
+    end
+
+    it_behaves_like 'logs kas error'
+  end
+
+  context 'when kas rpc fail' do
+    let(:error) { GRPC::Unavailable.new("failed to connect to all addresses") }
+
+    before do
+      allow_next_instance_of(Gitlab::Kas::Client) do |instance|
+        allow(instance).to receive(:get_server_info).and_raise(error)
+      end
+    end
+
+    it_behaves_like 'logs kas error'
+  end
+
+  describe '#version' do
+    subject { server_info.version }
+
+    it 'returns version' do
+      is_expected.to eq('17.4.0-rc1')
+    end
+  end
+
+  describe '#retrieved_server_info?' do
+    it { is_expected.to be_retrieved_server_info }
+
+    it 'returns false when server info is not retrieved' do
+      allow_next_instance_of(Gitlab::Kas::Client) do |instance|
+        allow(instance).to receive(:get_server_info).and_raise(Gitlab::Kas::Client::ConfigurationError)
+      end
+
+      is_expected.not_to be_retrieved_server_info
+    end
+  end
+
+  describe '#version_info' do
+    subject { server_info.version_info.to_s }
+
+    context 'when git ref is a commit' do
+      let(:git_ref) { '6a0281c68969d9ce8f36fdaf242b4f6e0503d940' }
+
+      it { is_expected.to eq('17.4.0-rc1+6a0281c68969d9ce8f36fdaf242b4f6e0503d940') }
+    end
+
+    context 'when git ref is a tag' do
+      let(:git_ref) { 'v17.4.0-rc1' }
+
+      it { is_expected.to eq('17.4.0') }
+    end
+
+    context 'when git ref is empty' do
+      let(:git_ref) { '' }
+
+      it { is_expected.to eq('17.4.0') }
+    end
+  end
+end
diff --git a/spec/models/app_config/kas_metadata_spec.rb b/spec/models/app_config/kas_metadata_spec.rb
index 3c8a490bdecf..e863913a37d3 100644
--- a/spec/models/app_config/kas_metadata_spec.rb
+++ b/spec/models/app_config/kas_metadata_spec.rb
@@ -3,6 +3,8 @@
 require 'spec_helper'
 
 RSpec.describe AppConfig::KasMetadata, feature_category: :api do
+  let(:kas_version_info) { instance_double(Gitlab::VersionInfo) }
+
   it 'has InstanceMetadataPolicy as declarative policy' do
     expect(described_class.declarative_policy_class).to eq("AppConfig::InstanceMetadataPolicy")
   end
@@ -10,10 +12,13 @@
   context 'when KAS is enabled' do
     it 'has the correct properties' do
       allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+      allow_next_instance_of(Gitlab::Kas::ServerInfo) do |instance|
+        allow(instance).to receive(:version_info).and_return(kas_version_info)
+      end
 
       expect(described_class.new).to have_attributes(
         enabled: Gitlab::Kas.enabled?,
-        version: Gitlab::Kas.version,
+        version: kas_version_info,
         external_url: Gitlab::Kas.external_url,
         external_k8s_proxy_url: Gitlab::Kas.tunnel_url
       )
diff --git a/spec/presenters/gitlab/kas/server_info_presenter_spec.rb b/spec/presenters/gitlab/kas/server_info_presenter_spec.rb
index 35219da6afd5..76b5e41732f6 100644
--- a/spec/presenters/gitlab/kas/server_info_presenter_spec.rb
+++ b/spec/presenters/gitlab/kas/server_info_presenter_spec.rb
@@ -4,71 +4,14 @@
 
 RSpec.describe Gitlab::Kas::ServerInfoPresenter, feature_category: :deployment_management do
   let(:git_ref) { '6a0281c68969d9ce8f36fdaf242b4f6e0503d940' }
-  let(:presenter) { described_class.new }
-
-  before do
-    allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
-
-    # rubocop:disable RSpec/VerifiedDoubles -- Avoiding the false positive 'the Gitlab::Agent::ServerInfo::ServerInfo
-    # class does not implement the instance method: version' when using instance_double(), which is only because
-    # Ruby protobuf handle these message through method_missing instead of actually defining instance methods.
-    response = double(Gitlab::Agent::ServerInfo::ServerInfo, version: '17.4.0-rc1', git_ref: git_ref)
-    # rubocop:enable RSpec/VerifiedDoubles
-
-    allow_next_instance_of(Gitlab::Kas::Client) do |instance|
-      allow(instance).to receive(:get_server_info).and_return(response)
-    end
-  end
-
-  shared_examples 'logs kas error' do
-    it 'logs the error' do
-      expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error)
-
-      presenter
-    end
-  end
-
-  context 'when kas client initialization fails' do
-    let(:error) { Gitlab::Kas::Client::ConfigurationError.new('boom') }
-
-    before do
-      allow(Gitlab::Kas::Client).to receive(:new).and_raise(error)
-    end
-
-    it_behaves_like 'logs kas error'
-  end
-
-  context 'when kas rpc fail' do
-    let(:error) { GRPC::Unavailable.new("failed to connect to all addresses") }
-
-    before do
-      allow_next_instance_of(Gitlab::Kas::Client) do |instance|
-        allow(instance).to receive(:get_server_info).and_raise(error)
-      end
-    end
-
-    it_behaves_like 'logs kas error'
+  let(:server_info) do
+    instance_double(Gitlab::Kas::ServerInfo,
+      retrieved_server_info?: true,
+      version: '17.4.0-rc1',
+      git_ref: git_ref)
   end
 
-  describe '#version' do
-    it 'returns version' do
-      expect(presenter.version).to eq('17.4.0-rc1')
-    end
-  end
-
-  describe '#retrieved_server_info?' do
-    it 'returns true when server info is retrieved' do
-      expect(presenter.retrieved_server_info?).to be(true)
-    end
-
-    it 'returns false when server info is not retrieved' do
-      allow_next_instance_of(Gitlab::Kas::Client) do |instance|
-        allow(instance).to receive(:get_server_info).and_raise(Gitlab::Kas::Client::ConfigurationError)
-      end
-
-      expect(presenter.retrieved_server_info?).to be(false)
-    end
-  end
+  let(:presenter) { described_class.new(server_info) }
 
   describe '#git_ref_for_display' do
     subject { presenter.git_ref_for_display }
@@ -90,6 +33,12 @@
 
       it { is_expected.to be_nil }
     end
+
+    context 'when server_info is not retrieved' do
+      let(:server_info) { instance_double(Gitlab::Kas::ServerInfo, retrieved_server_info?: false) }
+
+      it { is_expected.to be_nil }
+    end
   end
 
   describe '#git_ref_url' do
@@ -120,5 +69,11 @@
 
       it { is_expected.to be_nil }
     end
+
+    context 'when server_info is not retrieved' do
+      let(:server_info) { instance_double(Gitlab::Kas::ServerInfo, retrieved_server_info?: false) }
+
+      it { is_expected.to be_nil }
+    end
   end
 end
diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb
index d19fe9da797f..096515543cac 100644
--- a/spec/requests/api/graphql/metadata_query_spec.rb
+++ b/spec/requests/api/graphql/metadata_query_spec.rb
@@ -27,18 +27,44 @@
     end
 
     context 'kas is enabled' do
-      let(:expected_kas_version) { Gitlab::Kas.version }
+      let(:kas_version_info) { Gitlab::VersionInfo.new(1, 2, 3) }
+      let(:expected_kas_version) { kas_version_info.to_s }
       let(:expected_kas_external_url) { Gitlab::Kas.external_url }
       let(:expected_kas_external_k8s_proxy_url) { Gitlab::Kas.tunnel_url }
 
       before do
         allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
-        post_graphql(query, current_user: current_user)
       end
 
-      it 'returns version, revision, kas_enabled, kas_version, kas_external_url' do
-        expect(graphql_errors).to be_nil
-        expect(graphql_data).to eq(expected_data)
+      context 'when kas server info fetched successfully' do
+        before do
+          allow_next_instance_of(Gitlab::Kas::ServerInfo) do |server_info|
+            allow(server_info).to receive(:version_info).and_return(kas_version_info)
+          end
+          post_graphql(query, current_user: current_user)
+        end
+
+        it 'returns version, revision, kas_enabled, kas_version, kas_external_url' do
+          expect(graphql_errors).to be_nil
+          expect(graphql_data).to eq(expected_data)
+        end
+      end
+
+      context 'when failed to fetch kas server info' do
+        let(:expected_kas_version) { nil }
+
+        before do
+          allow_next_instance_of(Gitlab::Kas::ServerInfo) do |server_info|
+            # Upon RPC failure, Gitlab::Kas::ServerInfo#version_info could return nil after reporting the error.
+            allow(server_info).to receive(:version_info).and_return nil
+          end
+          post_graphql(query, current_user: current_user)
+        end
+
+        it 'returns nil as kas version' do
+          expect(graphql_errors).to be_nil
+          expect(graphql_data).to eq(expected_data)
+        end
       end
     end
 
@@ -70,6 +96,7 @@
     let(:query) { graphql_query_for('metadata', {}, feature_flags_field) }
 
     before do
+      allow(Gitlab::Kas).to receive(:enabled?).and_return(false)
       stub_feature_flag_definition('foo')
       stub_feature_flag_definition('ipsum')
       stub_feature_flag_definition('dolar')
@@ -115,5 +142,11 @@
       expect(graphql_errors).to be_nil
       expect(graphql_data).to eq('metadata' => nil)
     end
+
+    it 'avoids unnecessary kas query' do
+      post_graphql(query, current_user: nil)
+
+      expect(Gitlab::Kas).not_to receive(:enabled?)
+    end
   end
 end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 7a682b3bc952..9a18da14d66b 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -81,13 +81,14 @@
 
       before do
         server_info = instance_double(
-          Gitlab::Kas::ServerInfoPresenter,
+          Gitlab::Kas::ServerInfo,
           retrieved_server_info?: retrieved_server_info?,
-          version: '17.4.0-rc1',
-          git_ref_for_display: '6a0281c6896',
-          git_ref_url: 'some/url'
+          version: '17.4.0-rc1'
         )
-        assign(:kas_server_info, server_info)
+        presenter = Gitlab::Kas::ServerInfoPresenter.new(server_info)
+        allow(presenter).to receive(:git_ref_for_display).and_return('6a0281c6896')
+        allow(presenter).to receive(:git_ref_url).and_return('some/url')
+        assign(:kas_server_info, presenter)
       end
 
       context 'when successfully fetched KAS version' do
-- 
GitLab