From f18719c8f2e122b5ebab01c9b4ff738867fc3603 Mon Sep 17 00:00:00 2001
From: Manoj M J <mmj@gitlab.com>
Date: Thu, 3 Oct 2024 08:37:46 +0000
Subject: [PATCH] Add probe for checks in air-gapped instances

Add probe for checks in air-gapped instances
that self-host AI Gateway

Changelog: changed
EE: true
---
 doc/user/gitlab_duo/turn_on_off.md            | 13 +++-
 .../status_checks/probes/end_to_end_probe.rb  |  4 +-
 .../status_checks/probes/host_probe.rb        | 20 ++++-
 .../ai_gateway_url_presence_probe.rb          | 45 +++++++++++
 .../code_suggestions_license_probe.rb         | 68 +++++++++++++++++
 .../status_checks/status_service.rb           | 14 +++-
 ee/lib/gitlab/ai/self_hosted/ai_gateway.rb    | 29 ++++++++
 .../gitlab/ai/self_hosted/ai_gateway_spec.rb  | 45 +++++++++++
 .../probes/end_to_end_probe_spec.rb           |  4 +-
 .../status_checks/probes/host_probe_spec.rb   | 24 ++++++
 .../ai_gateway_url_presence_probe_spec.rb     | 35 +++++++++
 .../code_suggestions_license_probe_spec.rb    | 74 +++++++++++++++++++
 .../status_checks/status_service_spec.rb      | 24 +++++-
 locale/gitlab.pot                             | 25 ++++++-
 14 files changed, 411 insertions(+), 13 deletions(-)
 create mode 100644 ee/app/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe.rb
 create mode 100644 ee/app/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe.rb
 create mode 100644 ee/lib/gitlab/ai/self_hosted/ai_gateway.rb
 create mode 100644 ee/spec/lib/gitlab/ai/self_hosted/ai_gateway_spec.rb
 create mode 100644 ee/spec/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe_spec.rb
 create mode 100644 ee/spec/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe_spec.rb

diff --git a/doc/user/gitlab_duo/turn_on_off.md b/doc/user/gitlab_duo/turn_on_off.md
index 8276fa4184f27..ff70ee6d43e59 100644
--- a/doc/user/gitlab_duo/turn_on_off.md
+++ b/doc/user/gitlab_duo/turn_on_off.md
@@ -90,7 +90,10 @@ To run a health check:
 
 ### Health check tests
 
-The health check performs the following tests to verify if your instance meets the requirements to use GitLab Duo.
+To verify if your instance meets the requirements to use GitLab Duo, the health check performs tests
+for online and offline environments.
+
+#### For online environments
 
 | Test | Description |
 |-----------------|-------------|
@@ -98,6 +101,14 @@ The health check performs the following tests to verify if your instance meets t
 | Synchronization | Tests whether your subscription: <br>- Has been activated with an activation code and can be synchronized with `customers.gitlab.com`.<br>- Has correct access credentials.<br>- Has been synchronized recently. If it hasn't or the access credentials are missing or expired, you can [manually synchronize](../../subscriptions/self_managed/index.md#manually-synchronize-subscription-data) your subscription data. |
 | System exchange | Tests whether Code Suggestions can be used in your instance. If the system exchange assessment fails, users might not be able to use GitLab Duo features. |
 
+#### For offline environments
+
+| Test | Description |
+|-----------------|-------------|
+| Network | Tests whether: <br>- The environment variable `AI_GATEWAY_URL` has been set to a valid URL.<br> - Your instance can connect to the URL specified by `AI_GATEWAY_URL`.<br><br>If your instance cannot connect to the URL, ensure that your firewall or proxy server settings [allow connection](#configure-gitlab-duo-on-a-self-managed-instance). |
+| License | Tests whether your license has the ability to access Code Suggestions feature. |
+| System exchange | Tests whether Code Suggestions can be used in your instance. If the system exchange assessment fails, users might not be able to use GitLab Duo features. |
+
 ## Turn off GitLab Duo features
 
 You can turn off GitLab Duo for a group, project, or instance.
diff --git a/ee/app/services/cloud_connector/status_checks/probes/end_to_end_probe.rb b/ee/app/services/cloud_connector/status_checks/probes/end_to_end_probe.rb
index f507902ef0cd8..5ca29678be7ec 100644
--- a/ee/app/services/cloud_connector/status_checks/probes/end_to_end_probe.rb
+++ b/ee/app/services/cloud_connector/status_checks/probes/end_to_end_probe.rb
@@ -20,7 +20,7 @@ def initialize(user)
 
         override :success_message
         def success_message
-          _('Authentication with GitLab Cloud services succeeded.')
+          _('Authentication with the AI gateway services succeeded.')
         end
 
         def check_user_exists
@@ -35,7 +35,7 @@ def validate_code_completion_availability
         end
 
         def failure_text(error)
-          format(_('Authentication with GitLab Cloud services failed: %{error}'), error: error)
+          format(_('Authentication with the AI gateway services failed: %{error}'), error: error)
         end
       end
     end
diff --git a/ee/app/services/cloud_connector/status_checks/probes/host_probe.rb b/ee/app/services/cloud_connector/status_checks/probes/host_probe.rb
index 8f2b7c5eb1768..e9b919bbcb8c7 100644
--- a/ee/app/services/cloud_connector/status_checks/probes/host_probe.rb
+++ b/ee/app/services/cloud_connector/status_checks/probes/host_probe.rb
@@ -10,16 +10,32 @@ class HostProbe < BaseProbe
 
         attr_reader :host, :port
 
-        validate :validate_connection
+        validate :validate_connection, if: :prerequisites_for_valid_url_met?
 
         def initialize(service_url)
-          uri = URI.parse(service_url)
+          @service_url = service_url
+
+          return if @service_url.blank?
+
+          uri = URI.parse(@service_url)
           @host = uri.host
           @port = uri.port
         end
 
         private
 
+        def prerequisites_for_valid_url_met?
+          return true if @host.present? && @port.present?
+
+          if @service_url.present?
+            errors.add(:base, format(_('%{service_url} is not a valid URL.'), service_url: @service_url))
+          else
+            errors.add(:base, _('Cannot validate connection to host because the URL is empty.'))
+          end
+
+          false
+        end
+
         override :success_message
         def success_message
           format(_('%{host} reachable.'), host: @host)
diff --git a/ee/app/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe.rb b/ee/app/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe.rb
new file mode 100644
index 0000000000000..6b41c9da13228
--- /dev/null
+++ b/ee/app/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module CloudConnector
+  module StatusChecks
+    module Probes
+      module SelfHosted
+        class AiGatewayUrlPresenceProbe < BaseProbe
+          extend ::Gitlab::Utils::Override
+
+          ENV_VARIABLE_NAME = 'AI_GATEWAY_URL'
+
+          validate :check_ai_gateway_url_presence
+
+          private
+
+          def self_hosted_url
+            ::Gitlab::AiGateway.self_hosted_url
+          end
+
+          def check_ai_gateway_url_presence
+            return if self_hosted_url.present?
+
+            errors.add(:base, failure_message)
+          end
+
+          override :success_message
+          def success_message
+            format(
+              _("Environment variable %{env_variable_name} is set to %{url}."),
+              env_variable_name: ENV_VARIABLE_NAME,
+              url: self_hosted_url
+            )
+          end
+
+          def failure_message
+            format(
+              _("Environment variable %{env_variable_name} is not set."),
+              env_variable_name: ENV_VARIABLE_NAME
+            )
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe.rb b/ee/app/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe.rb
new file mode 100644
index 0000000000000..fa18cfdc5ee6f
--- /dev/null
+++ b/ee/app/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module CloudConnector
+  module StatusChecks
+    module Probes
+      module SelfHosted
+        class CodeSuggestionsLicenseProbe < BaseProbe
+          extend ::Gitlab::Utils::Override
+
+          validate :check_user_exists
+          validate :validate_code_suggestions_availability
+
+          after_validation :collect_instance_details, :collect_license_details
+
+          def initialize(user)
+            @user = user
+          end
+
+          private
+
+          attr_reader :user
+
+          def check_user_exists
+            errors.add(:base, 'User not provided.') unless user
+          end
+
+          override :success_message
+          def success_message
+            _('License includes access to Code Suggestions.')
+          end
+
+          def validate_code_suggestions_availability
+            return unless user
+            return if Ability.allowed?(user, :access_code_suggestions)
+
+            if ::License.feature_available?(:code_suggestions)
+              text = _(
+                'License includes access to Code Suggestions, but you lack the necessary ' \
+                  'permissions to use this feature.'
+              )
+
+              errors.add(:base, text)
+
+              return
+            end
+
+            errors.add(:base, _('License does not provide access to Code Suggestions.'))
+          end
+
+          def collect_instance_details
+            details.add(:instance_id, Gitlab::GlobalAnonymousId.instance_id)
+            details.add(:gitlab_version, Gitlab::VERSION)
+          end
+
+          def collect_license_details
+            return unless license
+
+            details.add(:license, license.license.as_json)
+          end
+
+          def license
+            @license ||= License.current
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/cloud_connector/status_checks/status_service.rb b/ee/app/services/cloud_connector/status_checks/status_service.rb
index e541e96df91d4..2954d1ab2ba9a 100644
--- a/ee/app/services/cloud_connector/status_checks/status_service.rb
+++ b/ee/app/services/cloud_connector/status_checks/status_service.rb
@@ -13,7 +13,7 @@ class StatusService
 
       def initialize(user:, probes: nil)
         @user = user
-        @probes = probes || build_default_probes
+        @probes = probes || selected_probes
       end
 
       def execute
@@ -28,7 +28,17 @@ def execute
 
       private
 
-      def build_default_probes
+      def selected_probes
+        # An air-gapped instance, which requires that they run their own self-hosted AI Gateway,
+        # requires a different set of probes to be executed.
+        if ::Gitlab::Ai::SelfHosted::AiGateway.required?
+          ::Gitlab::Ai::SelfHosted::AiGateway.probes(@user)
+        else
+          default_probes
+        end
+      end
+
+      def default_probes
         [
           CloudConnector::StatusChecks::Probes::LicenseProbe.new,
           CloudConnector::StatusChecks::Probes::HostProbe.new(CUSTOMERS_DOT_URL),
diff --git a/ee/lib/gitlab/ai/self_hosted/ai_gateway.rb b/ee/lib/gitlab/ai/self_hosted/ai_gateway.rb
new file mode 100644
index 0000000000000..1be1b209de196
--- /dev/null
+++ b/ee/lib/gitlab/ai/self_hosted/ai_gateway.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ai
+    module SelfHosted
+      module AiGateway
+        extend self
+
+        # An instance having an offline cloud license is
+        # supposed to be an air-gapped instance.
+        # Air-gapped instances cannot connect to GitLab's default CloudConnector
+        # and are hence required to self-host their own AI Gateway (and the models)
+        def required?
+          ::Feature.enabled?(:ai_custom_model) && # rubocop:disable Gitlab/FeatureFlagWithoutActor -- The feature flag is global
+            ::License.current&.offline_cloud_license?
+        end
+
+        def probes(user)
+          [
+            ::CloudConnector::StatusChecks::Probes::SelfHosted::AiGatewayUrlPresenceProbe.new,
+            ::CloudConnector::StatusChecks::Probes::HostProbe.new(::Gitlab::AiGateway.self_hosted_url),
+            ::CloudConnector::StatusChecks::Probes::SelfHosted::CodeSuggestionsLicenseProbe.new(user),
+            ::CloudConnector::StatusChecks::Probes::EndToEndProbe.new(user)
+          ]
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/lib/gitlab/ai/self_hosted/ai_gateway_spec.rb b/ee/spec/lib/gitlab/ai/self_hosted/ai_gateway_spec.rb
new file mode 100644
index 0000000000000..bb84ce4467a82
--- /dev/null
+++ b/ee/spec/lib/gitlab/ai/self_hosted/ai_gateway_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ai::SelfHosted::AiGateway, feature_category: :"self-hosted_models" do
+  describe '.required?' do
+    context 'when the license is not an offline cloud license' do
+      it 'returns false' do
+        expect(described_class.required?).to be(false)
+      end
+    end
+
+    context 'when the license is an offline cloud license' do
+      before do
+        allow(::License).to receive_message_chain(:current, :offline_cloud_license?).and_return(true)
+      end
+
+      it 'returns true' do
+        expect(described_class.required?).to be(true)
+      end
+
+      context 'when the feature flag :ai_custom_model is disabled' do
+        it 'returns false' do
+          stub_feature_flags(ai_custom_model: false)
+          expect(described_class.required?).to be(false)
+        end
+      end
+    end
+  end
+
+  describe '.probes' do
+    let(:user) { build(:user) }
+
+    it 'returns an array with all expected probe instances' do
+      probes = described_class.probes(user)
+
+      expect(probes).to contain_exactly(
+        an_instance_of(::CloudConnector::StatusChecks::Probes::SelfHosted::AiGatewayUrlPresenceProbe),
+        an_instance_of(::CloudConnector::StatusChecks::Probes::HostProbe),
+        an_instance_of(::CloudConnector::StatusChecks::Probes::SelfHosted::CodeSuggestionsLicenseProbe),
+        an_instance_of(::CloudConnector::StatusChecks::Probes::EndToEndProbe)
+      )
+    end
+  end
+end
diff --git a/ee/spec/services/cloud_connector/status_checks/probes/end_to_end_probe_spec.rb b/ee/spec/services/cloud_connector/status_checks/probes/end_to_end_probe_spec.rb
index 33c001df471ed..05526d96f99c6 100644
--- a/ee/spec/services/cloud_connector/status_checks/probes/end_to_end_probe_spec.rb
+++ b/ee/spec/services/cloud_connector/status_checks/probes/end_to_end_probe_spec.rb
@@ -29,7 +29,7 @@
         result = probe.execute
 
         expect(result.success).to be true
-        expect(result.message).to match('Authentication with GitLab Cloud services succeeded')
+        expect(result.message).to match('Authentication with the AI gateway services succeeded')
       end
     end
 
@@ -46,7 +46,7 @@
         result = probe.execute
 
         expect(result.success).to be false
-        expect(result.message).to match("Authentication with GitLab Cloud services failed: #{error_message}")
+        expect(result.message).to match("Authentication with the AI gateway services failed: #{error_message}")
       end
     end
   end
diff --git a/ee/spec/services/cloud_connector/status_checks/probes/host_probe_spec.rb b/ee/spec/services/cloud_connector/status_checks/probes/host_probe_spec.rb
index 8fa46a1a6f05a..1574ad20e5631 100644
--- a/ee/spec/services/cloud_connector/status_checks/probes/host_probe_spec.rb
+++ b/ee/spec/services/cloud_connector/status_checks/probes/host_probe_spec.rb
@@ -8,6 +8,30 @@
 
     let(:uri) { 'https://example.com' }
 
+    context 'when the host is nil' do
+      let(:uri) { nil }
+
+      it 'returns a failure result' do
+        result = probe.execute
+
+        expect(result).to be_a(CloudConnector::StatusChecks::Probes::ProbeResult)
+        expect(result.success?).to be false
+        expect(result.message).to match("Cannot validate connection to host because the URL is empty.")
+      end
+    end
+
+    context 'when the host is not a valid URL' do
+      let(:uri) { 'not_a_valid_url' }
+
+      it 'returns a failure result' do
+        result = probe.execute
+
+        expect(result).to be_a(CloudConnector::StatusChecks::Probes::ProbeResult)
+        expect(result.success?).to be false
+        expect(result.message).to match("not_a_valid_url is not a valid URL.")
+      end
+    end
+
     context 'when the host is reachable' do
       before do
         allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
diff --git a/ee/spec/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe_spec.rb b/ee/spec/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe_spec.rb
new file mode 100644
index 0000000000000..f5b4cdf2990d4
--- /dev/null
+++ b/ee/spec/services/cloud_connector/status_checks/probes/self_hosted/ai_gateway_url_presence_probe_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CloudConnector::StatusChecks::Probes::SelfHosted::AiGatewayUrlPresenceProbe, feature_category: :cloud_connector do
+  let(:probe) { described_class.new }
+
+  describe '#execute' do
+    context 'when AI_GATEWAY_URL is set' do
+      before do
+        stub_env('AI_GATEWAY_URL', 'https://ai-gateway.mycompany.com')
+      end
+
+      it 'returns a successful result' do
+        result = probe.execute
+        expect(result).to be_a(CloudConnector::StatusChecks::Probes::ProbeResult)
+        expect(result.success?).to be(true)
+        expect(result.message).to eq('Environment variable AI_GATEWAY_URL is set to https://ai-gateway.mycompany.com.')
+      end
+    end
+
+    context 'when AI_GATEWAY_URL is not set' do
+      before do
+        stub_env('AI_GATEWAY_URL', nil)
+      end
+
+      it 'returns a failed result' do
+        result = probe.execute
+        expect(result).to be_a(CloudConnector::StatusChecks::Probes::ProbeResult)
+        expect(result.success?).to be(false)
+        expect(result.message).to eq('Environment variable AI_GATEWAY_URL is not set.')
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe_spec.rb b/ee/spec/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe_spec.rb
new file mode 100644
index 0000000000000..539b05bab5b25
--- /dev/null
+++ b/ee/spec/services/cloud_connector/status_checks/probes/self_hosted/code_suggestions_license_probe_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CloudConnector::StatusChecks::Probes::SelfHosted::CodeSuggestionsLicenseProbe, feature_category: :cloud_connector do
+  let(:probe) { described_class.new(user) }
+  let(:user) { build(:user) }
+
+  describe '#execute' do
+    context 'when user has access to code suggestions' do
+      before do
+        allow(Ability).to receive(:allowed?).with(user, :access_code_suggestions).and_return(true)
+      end
+
+      it 'returns a success result' do
+        result = probe.execute
+
+        expect(result.success).to be true
+        expect(result.message).to match('License includes access to Code Suggestions.')
+      end
+    end
+
+    context 'when user does not have access to code suggestions' do
+      before do
+        stub_licensed_features(code_suggestions: true)
+        allow(Ability).to receive(:allowed?).with(user, :access_code_suggestions).and_return(false)
+      end
+
+      it 'returns a failure result' do
+        result = probe.execute
+
+        expect(result.success).to be false
+        expect(result.message).to match(
+          'License includes access to Code Suggestions, but you lack the necessary ' \
+            'permissions to use this feature.'
+        )
+      end
+    end
+
+    context 'when license does not provide access to code suggestions' do
+      before do
+        stub_licensed_features(code_suggestions: false)
+      end
+
+      it 'returns a failure result' do
+        result = probe.execute
+
+        expect(result.success).to be false
+        expect(result.message).to match('License does not provide access to Code Suggestions.')
+      end
+    end
+
+    context 'on collecting details' do
+      let(:license) { build(:license, cloud: false) }
+
+      before do
+        allow(License).to receive(:current).and_return(license)
+      end
+
+      it 'collects the instance details' do
+        result = probe.execute
+
+        expect(result.details[:instance_id]).to eq(Gitlab::GlobalAnonymousId.instance_id)
+        expect(result.details[:gitlab_version]).to eq(Gitlab::VERSION)
+      end
+
+      it 'collects the license details' do
+        result = probe.execute
+
+        expect(result.details[:license]).to eq(License.current.license.as_json)
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/cloud_connector/status_checks/status_service_spec.rb b/ee/spec/services/cloud_connector/status_checks/status_service_spec.rb
index 04f280fef1373..bdda854852553 100644
--- a/ee/spec/services/cloud_connector/status_checks/status_service_spec.rb
+++ b/ee/spec/services/cloud_connector/status_checks/status_service_spec.rb
@@ -11,9 +11,9 @@
   subject(:service) { described_class.new(user: user, probes: probes) }
 
   describe '#initialize' do
-    context 'when no probes are passed' do
-      subject(:service) { described_class.new(user: user) }
+    subject(:service) { described_class.new(user: user) }
 
+    context 'when no probes are passed' do
       it 'created default probes' do
         service_probes = service.probes
 
@@ -26,6 +26,26 @@
         expect(service_probes[5]).to be_an_instance_of(CloudConnector::StatusChecks::Probes::EndToEndProbe)
       end
     end
+
+    context 'when self-hosted AI Gateway is required' do
+      before do
+        allow(::Gitlab::Ai::SelfHosted::AiGateway).to receive(:required?).and_return(true)
+      end
+
+      it 'uses a different set of probes' do
+        service_probes = service.probes
+
+        expect(service_probes.count).to eq(4)
+        expect(service_probes[0]).to be_an_instance_of(
+          CloudConnector::StatusChecks::Probes::SelfHosted::AiGatewayUrlPresenceProbe
+        )
+        expect(service_probes[1]).to be_an_instance_of(CloudConnector::StatusChecks::Probes::HostProbe)
+        expect(service_probes[2]).to be_an_instance_of(
+          CloudConnector::StatusChecks::Probes::SelfHosted::CodeSuggestionsLicenseProbe
+        )
+        expect(service_probes[3]).to be_an_instance_of(CloudConnector::StatusChecks::Probes::EndToEndProbe)
+      end
+    end
   end
 
   describe '#execute' do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c2f7112db0479..e5d7470478f77 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1254,6 +1254,9 @@ msgid_plural "%{selectedProjectsCount} projects"
 msgstr[0] ""
 msgstr[1] ""
 
+msgid "%{service_url} is not a valid URL."
+msgstr ""
+
 msgid "%{size} B"
 msgstr ""
 
@@ -8028,10 +8031,10 @@ msgstr ""
 msgid "Authentication via WebAuthn device failed."
 msgstr ""
 
-msgid "Authentication with GitLab Cloud services failed: %{error}"
+msgid "Authentication with the AI gateway services failed: %{error}"
 msgstr ""
 
-msgid "Authentication with GitLab Cloud services succeeded."
+msgid "Authentication with the AI gateway services succeeded."
 msgstr ""
 
 msgid "Author"
@@ -10892,6 +10895,9 @@ msgstr ""
 msgid "Cannot skip two factor authentication setup"
 msgstr ""
 
+msgid "Cannot validate connection to host because the URL is empty."
+msgstr ""
+
 msgid "Capacity threshold"
 msgstr ""
 
@@ -21272,6 +21278,12 @@ msgstr ""
 msgid "Environment scope"
 msgstr ""
 
+msgid "Environment variable %{env_variable_name} is not set."
+msgstr ""
+
+msgid "Environment variable %{env_variable_name} is set to %{url}."
+msgstr ""
+
 msgid "Environment variables on this GitLab instance are configured to be %{help_link_start}protected%{help_link_end} by default."
 msgstr ""
 
@@ -32119,6 +32131,15 @@ msgstr ""
 msgid "License Compliance| Used by %{dependencies}"
 msgstr ""
 
+msgid "License does not provide access to Code Suggestions."
+msgstr ""
+
+msgid "License includes access to Code Suggestions, but you lack the necessary permissions to use this feature."
+msgstr ""
+
+msgid "License includes access to Code Suggestions."
+msgstr ""
+
 msgid "License key"
 msgstr ""
 
-- 
GitLab