diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js index 6e8f7542683..4d6d8ad833d 100644 --- a/app/assets/javascripts/graphql_shared/constants.js +++ b/app/assets/javascripts/graphql_shared/constants.js @@ -2,6 +2,7 @@ export const MINIMUM_SEARCH_LENGTH = 3; export const TYPENAME_BOARD = 'Board'; export const TYPENAME_CI_BUILD = 'Ci::Build'; +export const TYPENAME_CI_JOB_TOKEN_ACCESSIBLE_GROUP = 'CiJobTokenAccessibleGroup'; export const TYPENAME_CI_RUNNER = 'Ci::Runner'; export const TYPENAME_CI_PIPELINE = 'Ci::Pipeline'; export const TYPENAME_CI_STAGE = 'Ci::Stage'; diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue index 014b0a1d55d..efc465e56af 100644 --- a/app/assets/javascripts/token_access/components/inbound_token_access.vue +++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue @@ -13,7 +13,7 @@ import { import { createAlert } from '~/alert'; import { __, s__, n__, sprintf } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { TYPENAME_GROUP } from '~/graphql_shared/constants'; +import { TYPENAME_CI_JOB_TOKEN_ACCESSIBLE_GROUP } from '~/graphql_shared/constants'; import CrudComponent from '~/vue_shared/components/crud_component.vue'; import ConfirmActionModal from '~/vue_shared/components/confirm_action_modal.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -274,7 +274,7 @@ export default { async removeItem() { const { __typename, fullPath } = this.namespaceToRemove; const mutation = - __typename === TYPENAME_GROUP + __typename === TYPENAME_CI_JOB_TOKEN_ACCESSIBLE_GROUP ? inboundRemoveGroupCIJobTokenScopeMutation : inboundRemoveProjectCIJobTokenScopeMutation; diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 4eeaeafda29..358a67194f8 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -93,6 +93,12 @@ class GraphqlController < ApplicationController render_error(exception.message, status: :unprocessable_entity) end + rescue_from RateLimitedService::RateLimitedError do |e| + e.log_request(request, current_user) + + render_error(e.message, status: :too_many_requests) + end + rescue_from Gitlab::Auth::TooManyIps do |exception| log_exception(exception) diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb index 1b88e918ea0..582b6666178 100644 --- a/app/models/ci/runner_manager.rb +++ b/app/models/ci/runner_manager.rb @@ -61,6 +61,7 @@ module Ci validates :architecture, length: { maximum: 255 } validates :ip_address, length: { maximum: 1024 } validates :config, json_schema: { filename: 'ci_runner_config' } + validates :runtime_features, json_schema: { filename: 'ci_runner_runtime_features' } validate :no_sharding_key_id, if: :instance_type? @@ -144,7 +145,8 @@ module Ci # database after heartbeat write happens. # ::Gitlab::Database::LoadBalancing::SessionMap.current(load_balancer).without_sticky_writes do - values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {} + values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, + :executor, :runtime_features) || {} values.merge!(contacted_at: Time.current, creation_state: :finished) if update_contacted_at diff --git a/app/validators/json_schemas/ci_runner_runtime_features.json b/app/validators/json_schemas/ci_runner_runtime_features.json new file mode 100644 index 00000000000..cc33e3a4ced --- /dev/null +++ b/app/validators/json_schemas/ci_runner_runtime_features.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "CI Runner runtime features", + "type": "object", + "additionalProperties": { + "type": "boolean" + } +} diff --git a/config/feature_flags/gitlab_com_derisk/ci_runner_manager_runtime_features.yml b/config/feature_flags/gitlab_com_derisk/ci_runner_manager_runtime_features.yml new file mode 100644 index 00000000000..26bfc852c8f --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/ci_runner_manager_runtime_features.yml @@ -0,0 +1,9 @@ +--- +name: ci_runner_manager_runtime_features +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/519966 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181817 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/520116 +milestone: '17.10' +group: group::ci platform +type: gitlab_com_derisk +default_enabled: false diff --git a/db/migrate/20250221083525_add_runtime_features_to_ci_runner_machines.rb b/db/migrate/20250221083525_add_runtime_features_to_ci_runner_machines.rb new file mode 100644 index 00000000000..739fe0389ec --- /dev/null +++ b/db/migrate/20250221083525_add_runtime_features_to_ci_runner_machines.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class AddRuntimeFeaturesToCiRunnerMachines < Gitlab::Database::Migration[2.2] + include Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers + + milestone '17.10' + disable_ddl_transaction! + + TABLE_NAME = 'ci_runner_machines' + PARTITIONED_TABLE_NAME = 'ci_runner_machines_687967fa8a' + + def up + with_lock_retries do + add_column PARTITIONED_TABLE_NAME, :runtime_features, + :jsonb, default: {}, null: false, if_not_exists: true + end + + with_lock_retries do + add_column TABLE_NAME, :runtime_features, + :jsonb, default: {}, null: false, if_not_exists: true + end + + with_lock_retries do + current_primary_key = Array.wrap(connection.primary_key(TABLE_NAME)) + + drop_sync_trigger(TABLE_NAME) # rubocop:disable Migration/WithLockRetriesDisallowedMethod -- false positive + create_trigger_to_sync_tables(TABLE_NAME, PARTITIONED_TABLE_NAME, current_primary_key) + end + end + + def down + with_lock_retries do + remove_column TABLE_NAME, :runtime_features, if_exists: true + end + + with_lock_retries do + remove_column PARTITIONED_TABLE_NAME, :runtime_features, if_exists: true + end + + with_lock_retries do + current_primary_key = Array.wrap(connection.primary_key(TABLE_NAME)) + + drop_sync_trigger(TABLE_NAME) # rubocop:disable Migration/WithLockRetriesDisallowedMethod -- false positive + create_trigger_to_sync_tables(TABLE_NAME, PARTITIONED_TABLE_NAME, current_primary_key) + end + end +end diff --git a/db/schema_migrations/20250221083525 b/db/schema_migrations/20250221083525 new file mode 100644 index 00000000000..ebadcd8a062 --- /dev/null +++ b/db/schema_migrations/20250221083525 @@ -0,0 +1 @@ +48b71863089fffb310f1d2ddb5c48511418e162a9d58be50bfcfa17831d645d9 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 325a25ba463..aa95277340f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -958,7 +958,8 @@ ELSIF (TG_OP = 'UPDATE') THEN "architecture" = NEW."architecture", "revision" = NEW."revision", "ip_address" = NEW."ip_address", - "version" = NEW."version" + "version" = NEW."version", + "runtime_features" = NEW."runtime_features" WHERE ci_runner_machines_687967fa8a."id" = NEW."id"; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO ci_runner_machines_687967fa8a ("id", @@ -976,7 +977,8 @@ ELSIF (TG_OP = 'INSERT') THEN "architecture", "revision", "ip_address", - "version") + "version", + "runtime_features") VALUES (NEW."id", NEW."runner_id", NEW."sharding_key_id", @@ -992,7 +994,8 @@ ELSIF (TG_OP = 'INSERT') THEN NEW."architecture", NEW."revision", NEW."ip_address", - NEW."version"); + NEW."version", + NEW."runtime_features"); END IF; RETURN NULL; @@ -10985,6 +10988,7 @@ CREATE TABLE ci_runner_machines ( creation_state smallint DEFAULT 0 NOT NULL, runner_type smallint, sharding_key_id bigint, + runtime_features jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT check_1537c1f66f CHECK ((char_length(platform) <= 255)), CONSTRAINT check_5253913ae9 CHECK ((char_length(system_xid) <= 64)), CONSTRAINT check_6f45a91da7 CHECK ((char_length(version) <= 2048)), @@ -11011,6 +11015,7 @@ CREATE TABLE ci_runner_machines_687967fa8a ( revision text, ip_address text, version text, + runtime_features jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT check_3d8736b3af CHECK ((char_length(system_xid) <= 64)), CONSTRAINT check_5bad2a6944 CHECK ((char_length(revision) <= 255)), CONSTRAINT check_7dc4eee8a5 CHECK ((char_length(version) <= 2048)), @@ -14372,6 +14377,7 @@ CREATE TABLE group_type_ci_runner_machines_687967fa8a ( revision text, ip_address text, version text, + runtime_features jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT check_3d8736b3af CHECK ((char_length(system_xid) <= 64)), CONSTRAINT check_5bad2a6944 CHECK ((char_length(revision) <= 255)), CONSTRAINT check_7dc4eee8a5 CHECK ((char_length(version) <= 2048)), @@ -14968,6 +14974,7 @@ CREATE TABLE instance_type_ci_runner_machines_687967fa8a ( revision text, ip_address text, version text, + runtime_features jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT check_3d8736b3af CHECK ((char_length(system_xid) <= 64)), CONSTRAINT check_5bad2a6944 CHECK ((char_length(revision) <= 255)), CONSTRAINT check_7dc4eee8a5 CHECK ((char_length(version) <= 2048)), @@ -20137,6 +20144,7 @@ CREATE TABLE project_type_ci_runner_machines_687967fa8a ( revision text, ip_address text, version text, + runtime_features jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT check_3d8736b3af CHECK ((char_length(system_xid) <= 64)), CONSTRAINT check_5bad2a6944 CHECK ((char_length(revision) <= 255)), CONSTRAINT check_7dc4eee8a5 CHECK ((char_length(version) <= 2048)), diff --git a/doc/administration/gitlab_duo_self_hosted/troubleshooting.md b/doc/administration/gitlab_duo_self_hosted/troubleshooting.md index bbcf0720682..6ab6cc25e47 100644 --- a/doc/administration/gitlab_duo_self_hosted/troubleshooting.md +++ b/doc/administration/gitlab_duo_self_hosted/troubleshooting.md @@ -264,6 +264,29 @@ If you find that the AI gateway cannot make that request, this might be caused b To resolve this, contact your network administrator. +## Check if AI Gateway can make requests to your GitLab instance + +The GitLab instance defined in `AIGW_GITLAB_URL` must be accessible from the AI Gateway container for request authentication. +If the instance is not reachable (for example, because of proxy configuration errors), requests can fail with errors, such as the following: + +- ```shell + jose.exceptions.JWTError: Signature verification failed + ``` + +- ```shell + gitlab_cloud_connector.providers.CompositeProvider.CriticalAuthError: No keys founds in JWKS; are OIDC providers up? + ``` + +In this scenario, verify if `AIGW_GITLAB_URL` and `$AIGW_GITLAB_API_URL` are properly set to the container and accessible. +The following commands should be successful when run from the container: + +```shell +poetry run troubleshoot +curl "$AIGW_GITLAB_API_URL/projects" +``` + +If not successful, verify your network configurations. + ## The image's platform does not match the host When [finding the AI gateway release](../../install/install_ai_gateway.md#find-the-ai-gateway-release), diff --git a/doc/solutions/languages/rust/_index.md b/doc/solutions/languages/rust/_index.md index 253bada22fc..3cabc08c1f7 100644 --- a/doc/solutions/languages/rust/_index.md +++ b/doc/solutions/languages/rust/_index.md @@ -5,12 +5,10 @@ info: This page is owned by the Solutions Architecture team. title: Rust Language and Ecosystem Solutions Index --- -Learn how to GitLab supports the Rust ecosystem. +This page attempts to index the ways in which GitLab supports Rust. It does so whether the integration is the result of configuring general functionality, was built in to Rust or GitLab or is provided as a solution. Unless otherwise noted, all of this content applies to both GitLab.com and self-managed instances. -This page attempts to index the ways in which GitLab supports Rust. It does so whether the integration is the result of configuring general functionality, was built in to Rust or GitLab or is provided as a solution. - | Text Tag | Configuration / Built / Solution | Support/Maintenance | | ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | `[Rust Configuration]` | Integration accomplished by Configuring Existing Rust Functionality | Rust | diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb index ff585034ac6..6958c64490c 100644 --- a/lib/api/ci/helpers/runner.rb +++ b/lib/api/ci/helpers/runner.rb @@ -35,6 +35,7 @@ module API .merge(get_system_id_from_request) .merge(get_runner_config_from_request) .merge(get_runner_ip) + .merge(get_runner_features_from_request) end def get_system_id_from_request @@ -240,6 +241,12 @@ module API { config: attributes_for_keys(%w[gpus], params.dig('info', 'config')) } end + def get_runner_features_from_request + return {} unless Feature.enabled?(:ci_runner_manager_runtime_features, current_runner) + + { runtime_features: attributes_for_keys(%w[features], params['info'])['features'] }.compact + end + def metrics strong_memoize(:metrics) { ::Gitlab::Ci::Runner::Metrics.new } end diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js index 45f99c7348f..95ed442b1e1 100644 --- a/spec/frontend/token_access/inbound_token_access_spec.js +++ b/spec/frontend/token_access/inbound_token_access_spec.js @@ -499,9 +499,9 @@ describe('TokenAccess component', () => { }); describe.each` - type | mutation | handler - ${'Group'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler} - ${'Project'} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler} + type | mutation | handler + ${'CiJobTokenAccessibleGroup'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler} + ${'Project'} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler} `('remove $type', ({ type, mutation, handler }) => { describe('when remove button is clicked', () => { beforeEach(async () => { diff --git a/spec/lib/api/ci/helpers/runner_helpers_spec.rb b/spec/lib/api/ci/helpers/runner_helpers_spec.rb index e05be65bf19..8e17b193811 100644 --- a/spec/lib/api/ci/helpers/runner_helpers_spec.rb +++ b/spec/lib/api/ci/helpers/runner_helpers_spec.rb @@ -41,6 +41,7 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do let(:architecture) { 'arm' } let(:executor) { 'shell' } let(:config) { { 'gpus' => 'all' } } + let(:features) { { 'cancelable' => true, 'proxy' => false } } let(:runner_params) do { system_id: system_id, @@ -53,6 +54,7 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do 'architecture' => architecture, 'executor' => executor, 'config' => config, + 'features' => features, 'ignored' => 1 } } @@ -62,7 +64,7 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do it 'extracts the runner details', :aggregate_failures do expect(details.keys).to match_array( - %w[system_id name version revision platform architecture executor config ip_address] + %w[system_id name version revision platform architecture executor config ip_address runtime_features] ) expect(details['system_id']).to eq(system_id) expect(details['name']).to eq(name) @@ -73,6 +75,13 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do expect(details['executor']).to eq(executor) expect(details['config']).to eq(config) expect(details['ip_address']).to eq(ip_address) + expect(details['runtime_features']).to eq(features) + end + + context 'when the features are empty' do + let(:features) { {} } + + it { expect(details).not_to have_key('runtime_features') } end end diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb index 4c1be14ddf8..eee55b75d67 100644 --- a/spec/models/ci/runner_manager_spec.rb +++ b/spec/models/ci/runner_manager_spec.rb @@ -116,6 +116,22 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo end end end + + context 'when runner has runtime features' do + it 'is valid' do + runner_manager = build(:ci_runner_machine, runtime_features: { cancelable: true }) + + expect(runner_manager).to be_valid + end + end + + context 'when runner has an runtime features' do + it 'is invalid' do + runner_manager = build(:ci_runner_machine, runtime_features: { cancelable: 1 }) + + expect(runner_manager).not_to be_valid + end + end end describe 'status scopes', :freeze_time do @@ -489,6 +505,7 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo ip_address: '8.8.8.8', architecture: '18-bit', config: { gpus: "all" }, + runtime_features: { cancelable: true }, executor: executor, version: version } @@ -652,6 +669,7 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo .and change { runner_manager.reload.read_attribute(:architecture) } .and change { runner_manager.reload.read_attribute(:config) } .and change { runner_manager.reload.read_attribute(:executor_type) } + .and change { runner_manager.reload.read_attribute(:runtime_features) } end end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 425db1c296f..efeb8ecbdd1 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -693,6 +693,34 @@ RSpec.describe 'GraphQL', feature_category: :shared do end end + context 'when rate limited' do + let_it_be(:project) { create(:project, :public) } + + let(:input) do + { + 'projectPath' => project.full_path, + 'title' => 'new title', + 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s + } + end + + let(:mutation) { graphql_mutation(:workItemCreate, input) } + + before do + allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:too_many_requests) + + expect(graphql_errors.pluck('message')).to include( + 'This endpoint has been requested too many times. Try again later.' + ) + end + end + describe 'keyset pagination' do let_it_be(:project) { create(:project, :public) } let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: Time.now.change(usec: 200)) }