diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-17 03:08:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-17 03:08:20 +0300 |
commit | 3f98f1e47b16b2b1d7a2e8a86252e002c2496098 (patch) | |
tree | 197eb008d51c312f3fc06c1e4cd2ecdda69576ea | |
parent | d62742b0169769191b32038cf20445a47db3b287 (diff) |
Add latest changes from gitlab-org/gitlab@master
30 files changed, 504 insertions, 177 deletions
diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 3344bb3a06d..997dfde4271 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -199,7 +199,7 @@ semgrep-appsec-custom-rules: - | semgrep ci --gitlab-sast --metrics off --config "${CI_BUILDS_DIR}/sast-custom-rules" \ --include app --include lib --include workhorse \ - --exclude '*_test.go' --exclude spec --exclude qa > gl-sast-report.json || true + --exclude '*_test.go' --exclude spec --exclude qa --exclude tooling > gl-sast-report.json || true variables: CUSTOM_RULES_REPOSITORY: https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules.git artifacts: diff --git a/app/finders/ci/runner_managers_finder.rb b/app/finders/ci/runner_managers_finder.rb index f24be74bbeb..1f8a2b1c9c1 100644 --- a/app/finders/ci/runner_managers_finder.rb +++ b/app/finders/ci/runner_managers_finder.rb @@ -8,24 +8,34 @@ module Ci end def execute - items = runner_managers + items = ::Ci::RunnerManager.for_runner(runner) - filter_by_status(items) + items = by_status(items) + items = by_system_id(items) + + sort_items(items) end private attr_reader :runner, :params - def runner_managers - ::Ci::RunnerManager.for_runner(runner) - end - - def filter_by_status(items) + def by_status(items) status = params[:status] - return items if status.blank? + return items if status.nil? items.with_status(status) end + + def by_system_id(items) + system_id = params[:system_id] + return items if system_id.nil? + + items.with_system_xid(system_id) + end + + def sort_items(items) + items.order_id_desc + end end end diff --git a/app/graphql/resolvers/ci/runner_managers_resolver.rb b/app/graphql/resolvers/ci/runner_managers_resolver.rb new file mode 100644 index 00000000000..1bdd6cb64e7 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_managers_resolver.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerManagersResolver < BaseResolver + type Types::Ci::RunnerManagerType.connection_type, null: true + + argument :system_id, ::GraphQL::Types::String, + required: false, + description: 'Filter runner managers by system ID.' + + argument :status, ::Types::Ci::RunnerStatusEnum, + required: false, + description: 'Filter runner managers by status.' + + def resolve(**args) + BatchLoader::GraphQL.for(object.id).batch(key: args[:system_id]) do |runner_ids, loader| + runner_managers = + ::Ci::RunnerManagersFinder + .new(runner: runner_ids, params: args.slice(:system_id, :status)) + .execute + ::Preloaders::RunnerManagerPolicyPreloader.new(runner_managers, current_user).execute + + runner_managers_by_runner_id = runner_managers.group_by(&:runner_id) + + runner_ids.each do |runner_id| + runner_managers = Array.wrap(runner_managers_by_runner_id[runner_id]) + loader.call(runner_id, runner_managers) + end + end + end + end + end +end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index c9f92c05975..dcacfb48997 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -53,8 +53,7 @@ module Types field :groups, null: true, resolver: ::Resolvers::Ci::RunnerGroupsResolver, description: 'Groups the runner is associated with. For group runners only.' - field :id, ::Types::GlobalIDType[::Ci::Runner], null: false, - description: 'ID of the runner.' + field :id, ::Types::GlobalIDType[::Ci::Runner], null: false, description: 'ID of the runner.' field :ip_address, GraphQL::Types::String, null: true, deprecated: { reason: "Use field in `manager` object instead", milestone: '16.2' }, description: 'IP address of the runner.' @@ -77,7 +76,8 @@ module Types field :maintenance_note, GraphQL::Types::String, null: true, description: 'Runner\'s maintenance notes.' field :managers, ::Types::Ci::RunnerManagerType.connection_type, null: true, - description: 'Machines associated with the runner configuration.', + description: 'Runner managers associated with the runner configuration.', + resolver: Resolvers::Ci::RunnerManagersResolver, alpha: { milestone: '15.10' } field :maximum_timeout, GraphQL::Types::Int, null: true, description: 'Maximum timeout (in seconds) for jobs processed by the runner.' @@ -174,18 +174,6 @@ module Types end end - def managers - BatchLoader::GraphQL.for(runner.id).batch(key: :runner_managers) do |runner_ids, loader| - runner_managers_by_runner_id = - ::Ci::RunnerManager.for_runner(runner_ids).order_id_desc.group_by(&:runner_id) - - runner_ids.each do |runner_id| - runner_managers = Array.wrap(runner_managers_by_runner_id[runner_id]) - loader.call(runner_id, runner_managers) - end - end - end - def job_execution_status BatchLoader::GraphQL.for(runner.id).batch(key: :running_builds_exist) do |runner_ids, loader| statuses = ::Ci::Runner.id_in(runner_ids).with_running_builds.index_by(&:id) diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 2da28910af3..afa3c11dcba 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -1,6 +1,6 @@ = gitlab_ui_form_for [:admin, @group] do |f| = form_errors(@group) - = render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-mb-6' }) do |c| + = render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-pb-5 gl-mb-6' }) do |c| - c.with_title { _('Naming, visibility') } - c.with_description do = _('Update your group name, description, avatar, and visibility.') @@ -11,7 +11,7 @@ .form-group.gl-form-group{ role: 'group' } = f.label :avatar, _("Group avatar"), class: 'gl-display-block col-form-label' = render 'shared/choose_avatar_button', f: f - = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false + = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false = render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-pb-3 gl-mb-6' }) do |c| - c.with_title { _('Permissions and group features') } diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml index 114b32ca581..03fa0fee260 100644 --- a/app/views/admin/sessions/_signin_box.html.haml +++ b/app/views/admin/sessions/_signin_box.html.haml @@ -1,17 +1,14 @@ - if any_form_based_providers_enabled? - if crowd_enabled? .login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) } - .login-body - = render 'devise/sessions/new_crowd', render_remember_me: false, submit_message: _('Enter admin mode') + = render 'devise/sessions/new_crowd', render_remember_me: false, submit_message: _('Enter admin mode') - ldap_servers.each_with_index do |server, i| .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) } - .login-body - = render 'devise/sessions/new_ldap', server: server, render_remember_me: false, submit_message: _('Enter admin mode') + = render 'devise/sessions/new_ldap', server: server, render_remember_me: false, submit_message: _('Enter admin mode') = render_if_exists 'devise/sessions/new_smartcard' - if allow_admin_mode_password_authentication_for_web? .login-box.tab-pane{ id: 'login-pane', role: 'tabpanel', class: active_when(!any_form_based_providers_enabled?) } - .login-body - = render 'admin/sessions/new_base' + = render 'admin/sessions/new_base' diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml index 898d47f446a..2c4a1c2456a 100644 --- a/app/views/admin/sessions/two_factor.html.haml +++ b/app/views/admin/sessions/two_factor.html.haml @@ -6,8 +6,7 @@ .login-page .borderless .login-box.gl-p-5 - .login-body - - if current_user.two_factor_enabled? - = render 'admin/sessions/two_factor_otp' - - if current_user.two_factor_webauthn_enabled? - = render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path + - if current_user.two_factor_enabled? + = render 'admin/sessions/two_factor_otp' + - if current_user.two_factor_webauthn_enabled? + = render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index 0ce6d9b1095..eeec6e1fa40 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -1,23 +1,22 @@ = render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions' .login-box.gl-p-5 - .login-body - = gitlab_ui_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f| - .devise-errors - = render "devise/shared/error_messages", resource: resource - .form-group - = f.label :email - = f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', title: _('Please provide a valid email address.'), value: nil - .form-text.gl-text-secondary - - emails_link = link_to('', profile_emails_url, target: '_blank', rel: 'noopener noreferrer') - = safe_format(s_('Requires your primary GitLab email address. If you want to confirm a secondary email address, go to %{emails_link_start}Emails%{emails_link_end}'), tag_pair(emails_link, :emails_link_start, :emails_link_end)) + = gitlab_ui_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f| + .devise-errors + = render "devise/shared/error_messages", resource: resource + .form-group + = f.label :email + = f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', title: _('Please provide a valid email address.'), value: nil + .form-text.gl-text-secondary + - emails_link = link_to('', profile_emails_url, target: '_blank', rel: 'noopener noreferrer') + = safe_format(s_('Requires your primary GitLab email address. If you want to confirm a secondary email address, go to %{emails_link_start}Emails%{emails_link_end}'), tag_pair(emails_link, :emails_link_start, :emails_link_end)) - %div - - if recaptcha_enabled? - = recaptcha_tags nonce: content_security_policy_nonce + %div + - if recaptcha_enabled? + = recaptcha_tags nonce: content_security_policy_nonce - .gl-mt-5 - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true) do - = _("Resend") + .gl-mt-5 + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true) do + = _("Resend") .clearfix.prepend-top-20 = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 7dd4d119a62..ea5ab378291 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -1,20 +1,19 @@ = render 'devise/shared/tab_single', tab_title: _('Change your password') .login-box - .login-body - = gitlab_ui_form_for(resource, as: resource_name, url: password_path(:user), html: { method: :put, class: 'gl-show-field-errors gl-pt-5' }) do |f| - .devise-errors.gl-px-5 - = render "devise/shared/error_messages", resource: resource - = f.hidden_field :reset_password_token - .form-group.gl-px-5 - = f.label _('New password'), for: "user_password" - = f.password_field :password, autocomplete: 'new-password', class: "form-control gl-form-input top js-password-complexity-validation", required: true, title: _('This field is required.'), data: { testid: 'password-field'} - = render_if_exists 'shared/password_requirements_list' - .form-group.gl-px-5 - = f.label _('Confirm new password'), for: "user_password_confirmation" - = f.password_field :password_confirmation, autocomplete: 'new-password', class: "form-control gl-form-input bottom", title: _('This field is required.'), data: { testid: 'password-confirmation-field' }, required: true - .clearfix.gl-px-5.gl-pb-5 - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'change-password-button' } }) do - = _('Change your password') + = gitlab_ui_form_for(resource, as: resource_name, url: password_path(:user), html: { method: :put, class: 'gl-show-field-errors gl-pt-5' }) do |f| + .devise-errors.gl-px-5 + = render "devise/shared/error_messages", resource: resource + = f.hidden_field :reset_password_token + .form-group.gl-px-5 + = f.label _('New password'), for: "user_password" + = f.password_field :password, autocomplete: 'new-password', class: "form-control gl-form-input top js-password-complexity-validation", required: true, title: _('This field is required.'), data: { testid: 'password-field'} + = render_if_exists 'shared/password_requirements_list' + .form-group.gl-px-5 + = f.label _('Confirm new password'), for: "user_password_confirmation" + = f.password_field :password_confirmation, autocomplete: 'new-password', class: "form-control gl-form-input bottom", title: _('This field is required.'), data: { testid: 'password-confirmation-field' }, required: true + .clearfix.gl-px-5.gl-pb-5 + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'change-password-button' } }) do + = _('Change your password') .clearfix.prepend-top-20 %p diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 536d4c9fd4b..b8f47608432 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -1,19 +1,18 @@ .login-box - .login-body - = gitlab_ui_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-p-5 gl-show-field-errors', aria: { live: 'assertive' }}) do |f| - .devise-errors - = render "devise/shared/error_messages", resource: resource - .form-group - = f.label :email, _('Email') - = f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.') - .form-text.text-muted - = _('Requires your primary or verified secondary GitLab email address.') + = gitlab_ui_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-p-5 gl-show-field-errors', aria: { live: 'assertive' }}) do |f| + .devise-errors + = render "devise/shared/error_messages", resource: resource + .form-group + = f.label :email, _('Email') + = f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.') + .form-text.text-muted + = _('Requires your primary or verified secondary GitLab email address.') - - if recaptcha_enabled? - .gl-mb-5 - = recaptcha_tags nonce: content_security_policy_nonce + - if recaptcha_enabled? + .gl-mb-5 + = recaptcha_tags nonce: content_security_policy_nonce - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true) do - = _('Reset password') + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true) do + = _('Reset password') = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/sessions/email_verification.haml b/app/views/devise/sessions/email_verification.haml index 085204fb6bf..290cdc47d80 100644 --- a/app/views/devise/sessions/email_verification.haml +++ b/app/views/devise/sessions/email_verification.haml @@ -1,8 +1,7 @@ %div = render 'devise/shared/tab_single', tab_title: s_('IdentityVerification|Help us protect your account') .login-box.gl-p-5 - .login-body - .js-email-verification{ data: verification_data(resource) } + .js-email-verification{ data: verification_data(resource) } %p.gl-p-5.gl-text-secondary - support_link_start = '<a href="https://about.gitlab.com/support/" target="_blank" rel="noopener noreferrer">'.html_safe = s_("IdentityVerification|If you've lost access to the email associated to this account or having trouble with the code, %{link_start}here are some other steps you can take.%{link_end}").html_safe % { link_start: support_link_start, link_end: '</a>'.html_safe } diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 454b89e40f8..c39e6230f69 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,19 +1,18 @@ .login-box.gl-p-5 - .login-body - - if @user.two_factor_enabled? - = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_enabled?}", aria: { live: 'assertive' }}) do |f| - .form-group - = f.label :otp_attempt, _('Enter verification code') - = f.text_field :otp_attempt, class: 'form-control gl-form-input', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.'), data: { testid: 'two-fa-code-field' } - %p.form-text.text-muted.hint - = _("Enter the code from your two-factor authenticator app. If you've lost your device, you can enter one of your recovery codes.") + - if @user.two_factor_enabled? + = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_enabled?}", aria: { live: 'assertive' }}) do |f| + .form-group + = f.label :otp_attempt, _('Enter verification code') + = f.text_field :otp_attempt, class: 'form-control gl-form-input', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.'), data: { testid: 'two-fa-code-field' } + %p.form-text.text-muted.hint + = _("Enter the code from your two-factor authenticator app. If you've lost your device, you can enter one of your recovery codes.") - - if remember_me_enabled? - - resource_params = params[resource_name].presence || params - = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) + - if remember_me_enabled? + - resource_params = params[resource_name].presence || params + = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'verify-code-button' } }) do - = _("Verify code") + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'verify-code-button' } }) do + = _("Verify code") - - if @user.two_factor_webauthn_enabled? - = render "authentication/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path + - if @user.two_factor_webauthn_enabled? + = render "authentication/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 60f1ff02e76..a5251f9e182 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,22 +1,18 @@ - if any_form_based_providers_enabled? - if crowd_enabled? .login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) } - .login-body - = render 'devise/sessions/new_crowd' + = render 'devise/sessions/new_crowd' - ldap_servers.each_with_index do |server, i| .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) } - .login-body - = render 'devise/sessions/new_ldap', server: server + = render 'devise/sessions/new_ldap', server: server = render_if_exists 'devise/sessions/new_smartcard' - if password_authentication_enabled_for_web? .login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' } - .login-body - = render 'devise/sessions/new_base' + = render 'devise/sessions/new_base' - elsif password_authentication_enabled_for_web? .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' } - .login-body - = render 'devise/sessions/new_base' + = render 'devise/sessions/new_base' diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml index 393f42cd197..b4d0a8f5b8e 100644 --- a/app/views/devise/unlocks/new.html.haml +++ b/app/views/devise/unlocks/new.html.haml @@ -1,15 +1,14 @@ = render 'devise/shared/tab_single', tab_title: _('Resend unlock instructions') .login-box - .login-body - = gitlab_ui_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-p-5 gl-show-field-errors' }) do |f| - .devise-errors - = render "devise/shared/error_messages", resource: resource - .form-group - = f.label :email, _('Email') - = f.email_field :email, class: 'form-control gl-form-input', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: _('Please provide a valid email address.') + = gitlab_ui_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-p-5 gl-show-field-errors' }) do |f| + .devise-errors + = render "devise/shared/error_messages", resource: resource + .form-group + = f.label :email, _('Email') + = f.email_field :email, class: 'form-control gl-form-input', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: _('Please provide a valid email address.') - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true) do - = _('Resend unlock instructions') + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true) do + = _('Resend unlock instructions') = render 'devise/shared/sign_in_link' diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index b64824bf509..7941f35fdbb 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -5,7 +5,7 @@ = render Pajamas::ButtonComponent.new(icon: 'ellipsis_v', category: :tertiary, button_options: { class: 'note-action-button more-actions-toggle has-tooltip', data: { title: 'More actions', toggle: 'dropdown', container: 'body', testid: 'more-actions-dropdown' }}) %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %li - = deprecated_clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true) + = clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard gl-rounded-0!', size: :medium, hide_tooltip: true, hide_button_icon: true) - unless is_current_user .gl-ml-n2 .js-report-abuse-dropdown-item{ data: { report_abuse_path: add_category_abuse_reports_path, reported_user_id: note.author.id, reported_from_url: noteable_note_url(note) } } diff --git a/app/views/shared/_old_visibility_level.html.haml b/app/views/shared/_old_visibility_level.html.haml deleted file mode 100644 index 6bcac2b0e6b..00000000000 --- a/app/views/shared/_old_visibility_level.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%fieldset.form-group.gl-form-group - %legend.col-form-label.col-form-label - = _('Visibility level') - = link_to sprite_icon('question-o'), help_page_path('user/public_access'), target: '_blank', rel: 'noopener noreferrer' - = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_visibility_level, form_model: form_model, with_label: with_label diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml index caab7710fa8..0aa8ad2628e 100644 --- a/app/views/shared/milestones/_delete_button.html.haml +++ b/app/views/shared/milestones/_delete_button.html.haml @@ -1,6 +1,6 @@ - milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone) -%button.gl-button.btn.btn-link.menu-item.js-delete-milestone-button{ data: { milestone_id: @milestone.id, milestone_title: markdown_field(@milestone, :title), milestone_url: milestone_url, milestone_issue_count: @milestone.issues.count, milestone_merge_request_count: @milestone.merge_requests.count }, disabled: true } += render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: 'menu-item js-delete-milestone-button', data: { milestone_id: @milestone.id, milestone_title: markdown_field(@milestone, :title), milestone_url: milestone_url, milestone_issue_count: @milestone.issues.count, milestone_merge_request_count: @milestone.merge_requests.count }, disabled: true }) do .gl-dropdown-item-text-wrapper.gl-text-red-500 = _('Delete') #js-delete-milestone-modal diff --git a/config/application.rb b/config/application.rb index 12c7444ef39..0634bbf5165 100644 --- a/config/application.rb +++ b/config/application.rb @@ -92,11 +92,8 @@ module Gitlab # This preload is required to: # - # 1. Convert legacy `database.yml`; + # 1. Support providing sensitive DB configuration through an external script; # 2. Include Geo post-deployment migrations settings; - # - # TODO: In 15.0, this preload can be wrapped in a Gitlab.ee block - # since we don't need to convert legacy `database.yml` anymore. config.class.prepend(::Gitlab::Patch::DatabaseConfig) # Settings in config/environments/* take precedence over those specified here. diff --git a/config/feature_flags/development/github_import_lock_user_finder.yml b/config/feature_flags/development/github_import_lock_user_finder.yml new file mode 100644 index 00000000000..812e899ae21 --- /dev/null +++ b/config/feature_flags/development/github_import_lock_user_finder.yml @@ -0,0 +1,8 @@ +--- +name: github_import_lock_user_finder +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141826 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/438336 +milestone: '16.9' +type: development +group: group::import and integrate +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1fa12bd1ee0..70d4d3cf7da 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -16048,7 +16048,6 @@ CI/CD variables for a project. | <a id="cirunnerlocked"></a>`locked` | [`Boolean`](#boolean) | Indicates the runner is locked. | | <a id="cirunnermaintenancenote"></a>`maintenanceNote` | [`String`](#string) | Runner's maintenance notes. | | <a id="cirunnermaintenancenotehtml"></a>`maintenanceNoteHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `maintenance_note`. | -| <a id="cirunnermanagers"></a>`managers` **{warning-solid}** | [`CiRunnerManagerConnection`](#cirunnermanagerconnection) | **Introduced** in 15.10. This feature is an Experiment. It can be changed or removed at any time. Machines associated with the runner configuration. | | <a id="cirunnermaximumtimeout"></a>`maximumTimeout` | [`Int`](#int) | Maximum timeout (in seconds) for jobs processed by the runner. | | <a id="cirunnerownerproject"></a>`ownerProject` | [`Project`](#project) | Project that owns the runner. For project runners only. | | <a id="cirunnerpaused"></a>`paused` | [`Boolean!`](#boolean) | Indicates the runner is paused and not available to run jobs. | @@ -16098,6 +16097,27 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="cirunnerjobsstatuses"></a>`statuses` | [`[CiJobStatus!]`](#cijobstatus) | Filter jobs by status. | +##### `CiRunner.managers` + +Runner managers associated with the runner configuration. + +WARNING: +**Introduced** in 15.10. +This feature is an Experiment. It can be changed or removed at any time. + +Returns [`CiRunnerManagerConnection`](#cirunnermanagerconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cirunnermanagersstatus"></a>`status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runner managers by status. | +| <a id="cirunnermanagerssystemid"></a>`systemId` | [`String`](#string) | Filter runner managers by system ID. | + ##### `CiRunner.projects` Find projects the runner is associated with. For project runners only. @@ -17319,7 +17339,7 @@ Represents a product analytics dashboard visualization. | <a id="customizablepermissionavailablefor"></a>`availableFor` | [`[String!]!`](#string) | Objects the permission is available for. | | <a id="customizablepermissiondescription"></a>`description` | [`String`](#string) | Description of the permission. | | <a id="customizablepermissionname"></a>`name` | [`String!`](#string) | Localized name of the permission. | -| <a id="customizablepermissionrequirement"></a>`requirement` | [`MemberRolePermission`](#memberrolepermission) | Requirement of the permission. | +| <a id="customizablepermissionrequirements"></a>`requirements` | [`[MemberRolePermission!]`](#memberrolepermission) | Requirements of the permission. | | <a id="customizablepermissionvalue"></a>`value` | [`MemberRolePermission!`](#memberrolepermission) | Value of the permission. | ### `DastPreScanVerification` diff --git a/doc/architecture/blueprints/tailwindcss/index.md b/doc/architecture/blueprints/tailwindcss/index.md index 0409f802038..6d58e0dae8e 100644 --- a/doc/architecture/blueprints/tailwindcss/index.md +++ b/doc/architecture/blueprints/tailwindcss/index.md @@ -1,5 +1,5 @@ --- -status: proposed +status: ongoing creation-date: "2023-12-21" authors: [ "@peterhegman", "@svedova", "@pgascouvaillancourt" ] approvers: [ "@samdbeckham" ] diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md index 69f7c558b8b..9b3332bddba 100644 --- a/doc/ci/environments/protected_environments.md +++ b/doc/ci/environments/protected_environments.md @@ -27,7 +27,7 @@ Maintainer role. Prerequisites: - When granting the **Allowed to deploy** permission to a group or subgroup, the user configuring the protected environment must be a **direct member** of the group or subgroup to be added. Otherwise, the group or subgroup does not show up in the dropdown list. For more information see [issue #345140](https://gitlab.com/gitlab-org/gitlab/-/issues/345140). -- When granting **Allowed to deploy** and **Approvers** permissions to a group or project by using the settings UI, only direct members of the group or project receive these permissions. To grant these permissions to inherited members also, [use the API](../../api/protected_environments.md#group-inheritance-types). For more information see [issue #422392](https://gitlab.com/gitlab-org/gitlab/-/issues/422392). +- When granting **Allowed to deploy** permissions to a group or project by using the settings UI, only direct members of the group or project receive these permissions. To grant these permissions to inherited members also, [use the API](../../api/protected_environments.md#group-inheritance-types). For more information see [issue #422392](https://gitlab.com/gitlab-org/gitlab/-/issues/422392). To protect an environment: diff --git a/doc/development/permissions/custom_roles.md b/doc/development/permissions/custom_roles.md index 53589658f56..b8f6eba3340 100644 --- a/doc/development/permissions/custom_roles.md +++ b/doc/development/permissions/custom_roles.md @@ -320,6 +320,7 @@ To add a new custom ability: | `milestone` | yes | Milestone in which this custom ability was added. | | `group_ability` | yes | Indicate whether this ability is checked on group level. | | `project_ability` | yes | Indicate whether this ability is checked on project level. | +| `requirements` | no | The custom abilities that need to be enabled for this ability. | | `skip_seat_consumption` | yes | Indicate wheter this ability should be skiped when counting licensed users. | ### Privilege escalation consideration diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb index bec4c7fc4d4..fbde1778a89 100644 --- a/lib/gitlab/github_import/user_finder.rb +++ b/lib/gitlab/github_import/user_finder.rb @@ -130,20 +130,32 @@ module Gitlab email = read_email_from_cache(username) if email.blank? && !email_fetched_for_project?(username) - # If an ETAG is available, make an API call with the ETAG. - # Only make a rate-limited API call if the ETAG is not available and the email is nil. - etag = read_etag_from_cache(username) - email = fetch_email_from_github(username, etag: etag) || email - - cache_email!(username, email) - cache_etag!(username) if email.blank? && etag.nil? - - # If a non-blank email is cached, we don't need the ETAG or project check caches. - # Otherwise, indicate that the project has been checked. - if email.present? - clear_caches!(username) - else - set_project_as_checked!(username) + feature_flag_in_lock(lease_key, ttl: 3.minutes, sleep_sec: 1.second, retries: 30) do |retried| + # when retried, check the cache again as the other process that had the lease may have fetched the email + if retried + email = read_email_from_cache(username) + + # early return if the other process fetched a non-empty email. If the email is empty, we'll attempt to + # fetch it again in the lines below, but using the ETAG cached by the other process which won't count to + # the rate limit. + next email if email.present? + end + + # If an ETAG is available, make an API call with the ETAG. + # Only make a rate-limited API call if the ETAG is not available and the email is nil. + etag = read_etag_from_cache(username) + email = fetch_email_from_github(username, etag: etag) || email + + cache_email!(username, email) + cache_etag!(username) if email.blank? && etag.nil? + + # If a non-blank email is cached, we don't need the ETAG or project check caches. + # Otherwise, indicate that the project has been checked. + if email.present? + clear_caches!(username) + else + set_project_as_checked!(username) + end end end @@ -240,20 +252,11 @@ module Gitlab end def fetch_email_from_github(username, etag: nil) - in_lock(lease_key, ttl: 3.minutes, sleep_sec: 1.second, retries: 30) do |retried| - # when retried, check the cache again as the other process that had the lease may have fetched the email - if retried - email = read_email_from_cache(username) - - next email if email.present? - end + log(EMAIL_API_CALL_LOGGING_MESSAGE[etag.present?], username: username) - log(EMAIL_API_CALL_LOGGING_MESSAGE[etag.present?], username: username) - - # Only make a rate-limited API call if the ETAG is not available }) - user = client.user(username, { headers: { 'If-None-Match' => etag }.compact }) - user[:email] || '' if user - end + # Only make a rate-limited API call if the ETAG is not available }) + user = client.user(username, { headers: { 'If-None-Match' => etag }.compact }) + user[:email] || '' if user end # Caches the email associated to the username @@ -303,6 +306,14 @@ module Gitlab message: message ) end + + def feature_flag_in_lock(lease_key, ttl: 3.minutes, sleep_sec: 1.second, retries: 30) + return yield(false) if Feature.disabled?(:github_import_lock_user_finder, project.creator) + + in_lock(lease_key, ttl: ttl, sleep_sec: sleep_sec, retries: retries) do |retried| + yield(retried) + end + end end end end diff --git a/lib/gitlab/patch/database_config.rb b/lib/gitlab/patch/database_config.rb index 8a7566f6e0e..33be63a31c0 100644 --- a/lib/gitlab/patch/database_config.rb +++ b/lib/gitlab/patch/database_config.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../popen' + # The purpose of this code is to set the migrations path # for the Geo tracking database and the embedding database. module Gitlab @@ -7,25 +9,60 @@ module Gitlab module DatabaseConfig extend ActiveSupport::Concern + CommandExecutionError = Class.new(StandardError) + def database_configuration super.to_h do |env, configs| + parsed_config = parse_extra_config(configs) + if Gitlab.ee? ee_databases = %w[embedding geo] ee_databases.each do |ee_db_name| - next unless configs.key?(ee_db_name) + next unless parsed_config.key?(ee_db_name) - migrations_paths = Array(configs[ee_db_name]['migrations_paths']) + migrations_paths = Array(parsed_config[ee_db_name]['migrations_paths']) migrations_paths << File.join('ee', 'db', ee_db_name, 'migrate') if migrations_paths.empty? migrations_paths << File.join('ee', 'db', ee_db_name, 'post_migrate') unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] - configs[ee_db_name]['migrations_paths'] = migrations_paths.uniq - configs[ee_db_name]['schema_migrations_path'] = File.join('ee', 'db', ee_db_name, 'schema_migrations') if configs[ee_db_name]['schema_migrations_path'].blank? + parsed_config[ee_db_name]['migrations_paths'] = migrations_paths.uniq + parsed_config[ee_db_name]['schema_migrations_path'] = File.join('ee', 'db', ee_db_name, 'schema_migrations') if parsed_config[ee_db_name]['schema_migrations_path'].blank? end end - [env, configs] + [env, parsed_config] + end + end + + private + + def parse_extra_config(configs) + command = configs.delete('config_command') + return configs unless command.present? + + config_from_command = extra_config_from_command(command) + return configs unless config_from_command.present? + + configs.deep_merge(config_from_command) + end + + def extra_config_from_command(command) + cmd = command.split(" ") + output, exit_status = Gitlab::Popen.popen(cmd) + + if exit_status != 0 + raise CommandExecutionError, + "database.yml: Execution of `#{command}` failed with exit code #{exit_status}. Output: #{output}" end + + YAML.safe_load(output).deep_stringify_keys + rescue Psych::SyntaxError => e + error_message = <<~MSG + database.yml: Execution of `#{command}` generated invalid yaml. + Error: #{e.problem} #{e.context} at line #{e.line} column #{e.column} + MSG + + raise CommandExecutionError, error_message end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8f6ef41edb8..e262305d5be 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23234,6 +23234,9 @@ msgstr "" msgid "Group information" msgstr "" +msgid "Group inheritance" +msgstr "" + msgid "Group invite" msgstr "" @@ -39659,6 +39662,12 @@ msgstr "" msgid "ProtectedEnvironments|Edit" msgstr "" +msgid "ProtectedEnvironments|Enable group inheritance" +msgstr "" + +msgid "ProtectedEnvironments|Group inheritance" +msgstr "" + msgid "ProtectedEnvironments|Number of approvals must be between 1 and 5" msgstr "" diff --git a/spec/finders/ci/runner_managers_finder_spec.rb b/spec/finders/ci/runner_managers_finder_spec.rb index c62c05d415e..0581330e65b 100644 --- a/spec/finders/ci/runner_managers_finder_spec.rb +++ b/spec/finders/ci/runner_managers_finder_spec.rb @@ -65,13 +65,37 @@ RSpec.describe Ci::RunnerManagersFinder, '#execute', feature_category: :fleet_vi end end - context 'without any filters' do + describe 'filter by system_id' do + let_it_be(:runner_manager1) { create(:ci_runner_machine, runner: runner) } + let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner) } + + context "when system_id matches runner_manager1's" do + let(:params) { { system_id: runner_manager1.system_xid } } + + it { is_expected.to contain_exactly(runner_manager1) } + end + + context "when system_id matches runner_manager2's" do + let(:params) { { system_id: runner_manager2.system_xid } } + + it { is_expected.to contain_exactly(runner_manager2) } + end + + context "when system_id doesn't match" do + let(:params) { { system_id: 'non-matching' } } + + it { is_expected.to be_empty } + end + end + + context 'without any arguments' do let(:params) { {} } - let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) } + let_it_be(:runner_manager1) { create(:ci_runner_machine, runner: runner) } + let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner) } - it 'returns all runner managers' do - expect(runner_managers).to contain_exactly(runner_manager) + it 'returns all runner managers in id_desc order' do + expect(runner_managers).to eq([runner_manager2, runner_manager1]) end end end diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb index 998fa8b2c9f..e3415c12b6f 100644 --- a/spec/lib/gitlab/github_import/user_finder_spec.rb +++ b/spec/lib/gitlab/github_import/user_finder_spec.rb @@ -313,6 +313,19 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache, feat email_for_github_username end + context 'when github_import_lock_user_finder feature flag is disabled' do + before do + stub_feature_flags(github_import_lock_user_finder: false) + end + + it 'does not lock the finder' do + expect(finder).not_to receive(:in_lock) + expect(client).to receive(:user) + + email_for_github_username + end + end + context 'if the response contains an email' do before do allow(client).to receive(:user).and_return({ email: email }) @@ -478,8 +491,9 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache, feat it 'fetch user detail' do expect(finder).to receive(:read_email_from_cache).ordered.and_return('') expect(finder).to receive(:read_email_from_cache).ordered.and_return('') + expect(finder).to receive(:read_etag_from_cache).and_return(etag) expect(finder).to receive(:in_lock).and_yield(true) - expect(client).to receive(:user).with(username, { headers: {} }).and_return({ email: email }).once + expect(client).to receive(:user).with(username, { headers: { 'If-None-Match' => etag } }).once email_for_github_username end diff --git a/spec/lib/gitlab/patch/database_config_spec.rb b/spec/lib/gitlab/patch/database_config_spec.rb index b06d28dbcd5..73452853050 100644 --- a/spec/lib/gitlab/patch/database_config_spec.rb +++ b/spec/lib/gitlab/patch/database_config_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Patch::DatabaseConfig do expect(Rails::Application::Configuration).to include(described_class) end - describe 'config/database.yml' do + describe '#database_configuration' do let(:configuration) { Rails::Application::Configuration.new(Rails.root) } before do @@ -68,5 +68,94 @@ RSpec.describe Gitlab::Patch::DatabaseConfig do end include_examples 'hash containing main: connection name' + + context 'when config/database.yml contains extra configuration through an external command' do + let(:database_yml) do + <<-EOS + production: + config_command: '/opt/database-config.sh' + main: + adapter: postgresql + encoding: unicode + database: gitlabhq_production + username: git + password: "dummy password" + host: localhost + + development: + config_command: '/opt/database-config.sh' + main: + adapter: postgresql + encoding: unicode + database: gitlabhq_development + username: postgres + password: "dummy password" + host: localhost + variables: + statement_timeout: 15s + + test: &test + config_command: '/opt/database-config.sh' + main: + adapter: postgresql + encoding: unicode + database: gitlabhq_test + username: postgres + password: + host: localhost + prepared_statements: false + variables: + statement_timeout: 15s + EOS + end + + context 'when the external command returns valid yaml' do + before do + allow(Gitlab::Popen) + .to receive(:popen) + .and_return(["---\nmain:\n password: 'secure password'\n", 0]) + end + + it 'merges the extra configuration' do + database_configuration = configuration.database_configuration + + expect(database_configuration).to match( + "production" => { "main" => a_hash_including("password" => "secure password") }, + "development" => { "main" => a_hash_including("password" => "secure password") }, + "test" => { "main" => a_hash_including("password" => "secure password") } + ) + end + end + + context 'when the external command returns invalid yaml' do + before do + allow(Gitlab::Popen) + .to receive(:popen) + .and_return(["---\nmain:\n password: 'secure password\n", 0]) + end + + it 'raises an error' do + expect { configuration.database_configuration } + .to raise_error( + Gitlab::Patch::DatabaseConfig::CommandExecutionError, + %r{database.yml: Execution of `/opt/database-config.sh` generated invalid yaml} + ) + end + end + + context 'when the external command fails' do + before do + allow(Gitlab::Popen).to receive(:popen).and_return(["", 125]) + end + + it 'raises error' do + expect { configuration.database_configuration } + .to raise_error( + Gitlab::Patch::DatabaseConfig::CommandExecutionError, + %r{database.yml: Execution of `/opt/database-config.sh` failed} + ) + end + end + end end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 1b6948d0380..c528100dafa 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -213,22 +213,126 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibi end end - context 'with build running' do - let!(:pipeline) { create(:ci_pipeline, project: project1) } - let!(:runner_manager) do + context 'with runner managers' do + let_it_be(:runner) { create(:ci_runner) } + let_it_be(:runner_manager) do create(:ci_runner_machine, runner: runner, ip_address: '127.0.0.1', version: '16.3', revision: 'a', architecture: 'arm', platform: 'osx', contacted_at: 1.second.ago, executor_type: 'docker') end - let!(:runner) { create(:ci_runner) } - let!(:build) { create(:ci_build, :running, runner: runner, pipeline: pipeline) } + describe 'managers' do + let_it_be(:runner2) { create(:ci_runner) } + let_it_be(:runner_manager2_1) { create(:ci_runner_machine, runner: runner2) } + let_it_be(:runner_manager2_2) { create(:ci_runner_machine, runner: runner2) } + + context 'when filtering by status' do + let!(:offline_runner_manager) { create(:ci_runner_machine, runner: runner2, contacted_at: 2.hours.ago) } + let(:query) do + %( + query { + runner(id: "#{runner2.to_global_id}") { + id + managers(status: OFFLINE) { nodes { id } } + } + } + ) + end - before do - create(:ci_runner_machine_build, runner_manager: runner_manager, build: build) + it 'retrieves expected runner manager' do + post_graphql(query, current_user: user) + + expect(graphql_data).to match(a_hash_including( + 'runner' => a_graphql_entity_for( + 'managers' => { + 'nodes' => [a_graphql_entity_for(offline_runner_manager)] + } + ) + )) + end + end + + context 'fetching by runner ID and runner system ID' do + let(:query) do + %( + query { + runner1: runner(id: "#{runner.to_global_id}") { + id + managers(systemId: "#{runner_manager.system_xid}") { nodes { id } } + } + runner2: runner(id: "#{runner2.to_global_id}") { + id + managers(systemId: "#{runner_manager2_1.system_xid}") { nodes { id } } + } + } + ) + end + + it 'retrieves expected runner managers' do + post_graphql(query, current_user: user) + + expect(graphql_data).to match(a_hash_including( + 'runner1' => a_graphql_entity_for(runner, + 'managers' => a_hash_including('nodes' => [a_graphql_entity_for(runner_manager)])), + 'runner2' => a_graphql_entity_for(runner2, + 'managers' => a_hash_including('nodes' => [a_graphql_entity_for(runner_manager2_1)])) + )) + end + end + + context 'fetching runner ID and all runner managers' do + let(:query) do + %( + query { + runner(id: "#{runner2.to_global_id}") { id managers { nodes { id } } } + } + ) + end + + it 'retrieves expected runner managers' do + post_graphql(query, current_user: user) + + expect(graphql_data).to match(a_hash_including( + 'runner' => a_graphql_entity_for(runner2, + 'managers' => a_hash_including('nodes' => [ + a_graphql_entity_for(runner_manager2_2), a_graphql_entity_for(runner_manager2_1) + ])) + )) + end + end + + context 'fetching mismatched runner ID and system ID' do + let(:query) do + %( + query { + runner(id: "#{runner2.to_global_id}") { + id + managers(systemId: "#{runner_manager.system_xid}") { nodes { id } } + } + } + ) + end + + it 'retrieves expected runner managers' do + post_graphql(query, current_user: user) + + expect(graphql_data).to match(a_hash_including( + 'runner' => a_graphql_entity_for(runner2, 'managers' => a_hash_including('nodes' => [])) + )) + end + end end - it_behaves_like 'runner details fetch' + context 'with build running' do + let!(:pipeline) { create(:ci_pipeline, project: project1) } + let!(:build) { create(:ci_build, :running, runner: runner, pipeline: pipeline) } + + before do + create(:ci_runner_machine_build, runner_manager: runner_manager, build: build) + end + + it_behaves_like 'runner details fetch' + end end end |