diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-11 21:07:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-11 21:07:55 +0300 |
commit | 11df4bf91b8cf9ac7bb601241992e300eebf684c (patch) | |
tree | d3c2360dbd3edec006a09ed150267dc202020a91 | |
parent | 6282dd78339f98cbc5624e7fdf744a342d3d8b73 (diff) |
Add latest changes from gitlab-org/gitlab@master
122 files changed, 1507 insertions, 283 deletions
diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index a1bd57ce4f6..98928ce4715 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -5,6 +5,12 @@ include: - /ci/allure-report.yml - /ci/knapsack-report.yml +.bundler_variables: + variables: + BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true" + BUNDLE_SILENCE_ROOT_WARNING: "true" + BUNDLE_PATH: vendor + .test_variables: variables: QA_DEBUG: "true" @@ -21,16 +27,17 @@ include: - .use-docker-in-docker - .qa-cache - .test_variables + - .bundler_variables image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:debian-bullseye-ruby-2.7-bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23 stage: qa - needs: ["review-deploy"] + needs: + - review-deploy + - download-knapsack-report variables: DOCKER_HOST: tcp://docker:2376 DOCKER_TLS_CERTDIR: /certs DOCKER_CERT_PATH: /certs/client DOCKER_TLS_VERIFY: 1 - BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true" - BUNDLE_PATH: vendor before_script: - export EE_LICENSE="$(cat $REVIEW_APPS_EE_LICENSE_FILE)" - export QA_GITLAB_URL="$(cat environment_url.txt)" @@ -71,6 +78,25 @@ include: ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID ALLURE_RESULTS_GLOB: qa/tmp/allure-results/* +# Store knapsack report as artifact so the same report is reused across all jobs +download-knapsack-report: + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-99 + extends: + - .qa-cache + - .bundler_variables + - .review:rules:review-qa-reliable + stage: prepare + before_script: + - cd qa && bundle install + script: + - QA_KNAPSACK_REPORT_NAME=review-qa-reliable bundle exec rake "knapsack:download" + - QA_KNAPSACK_REPORT_NAME=review-qa-all bundle exec rake "knapsack:download" + allow_failure: true + artifacts: + paths: + - qa/knapsack/review-qa-*.json + expire_in: 1 day + review-qa-smoke: extends: - .review-qa-base @@ -145,7 +171,7 @@ allure-report-qa-all: variables: ALLURE_JOB_NAME: review-qa-all -knapsack-report: +upload-knapsack-report: extends: - .generate-knapsack-report-base stage: post-qa diff --git a/app/assets/javascripts/environments/components/environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue index d5c6d26cfd0..788c3ba6fed 100644 --- a/app/assets/javascripts/environments/components/environment_folder.vue +++ b/app/assets/javascripts/environments/components/environment_folder.vue @@ -34,9 +34,6 @@ export default { variables() { return { environment: this.nestedEnvironment.latest, scope: this.scope }; }, - pollInterval() { - return this.interval; - }, }, interval: { query: pollIntervalQuery, @@ -73,6 +70,11 @@ export default { methods: { toggleCollapse() { this.visible = !this.visible; + if (this.visible) { + this.$apollo.queries.folder.startPolling(this.interval); + } else { + this.$apollo.queries.folder.stopPolling(); + } }, isFirstEnvironment(index) { return index === 0; diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index a9948fed3b6..3432fe12705 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -386,7 +386,7 @@ export default { data-testid="add-to-review-button" type="submit" category="primary" - variant="success" + variant="confirm" @click.prevent="handleSaveDraft()" >{{ __('Add to review') }}</gl-button > diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue index dcdf89ae0d9..d3b6d07590f 100644 --- a/app/assets/javascripts/releases/components/tag_field_new.vue +++ b/app/assets/javascripts/releases/components/tag_field_new.vue @@ -52,7 +52,10 @@ export default { }, }, showTagNameValidationError() { - return this.isInputDirty && this.validationErrors.isTagNameEmpty; + return ( + this.isInputDirty && + (this.validationErrors.isTagNameEmpty || this.validationErrors.existingRelease) + ); }, tagNameInputId() { return uniqueId('tag-name-input-'); @@ -60,6 +63,11 @@ export default { createFromSelectorId() { return uniqueId('create-from-selector-'); }, + tagFeedback() { + return this.validationErrors.existingRelease + ? __('Selected tag is already in use. Choose another option.') + : __('Tag name is required.'); + }, }, methods: { ...mapActions('editNew', ['updateReleaseTagName', 'updateCreateFrom', 'fetchTagNotes']), @@ -112,7 +120,7 @@ export default { <gl-form-group data-testid="tag-name-field" :state="!showTagNameValidationError" - :invalid-feedback="__('Tag name is required')" + :invalid-feedback="tagFeedback" :label="$options.translations.tagName.label" :label-for="tagNameInputId" :label-description="$options.translations.tagName.labelDescription" diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js index 0a9bca97012..08197377f61 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js @@ -236,7 +236,7 @@ export const fetchTagNotes = ({ commit, state }, tagName) => { }) .catch((error) => { createFlash({ - message: s__('Release|Something went wrong while getting the tag notes.'), + message: s__('Release|Unable to fetch the tag notes.'), }); commit(types.RECEIVE_TAG_NOTES_ERROR, error); diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js index 036bf4f3eaf..0ca5eb9931a 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js @@ -53,6 +53,10 @@ export const validationErrors = (state) => { errors.isTagNameEmpty = true; } + if (state.existingRelease) { + errors.existingRelease = true; + } + // Each key of this object is a URL, and the value is an // array of Release link objects that share this URL. // This is used for detecting duplicate URLs. @@ -114,7 +118,11 @@ export const validationErrors = (state) => { /** Returns whether or not the release object is valid */ export const isValid = (_state, getters) => { const errors = getters.validationErrors; - return Object.values(errors.assets.links).every(isEmpty) && !errors.isTagNameEmpty; + return ( + Object.values(errors.assets.links).every(isEmpty) && + !errors.isTagNameEmpty && + !errors.existingRelease + ); }; /** Returns all the variables for a `releaseUpdate` GraphQL mutation */ diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js index 38153e4c67b..6b22468bbfe 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js @@ -103,6 +103,7 @@ export default { state.fetchError = undefined; state.isFetchingTagNotes = false; state.tagNotes = data.message; + state.existingRelease = data.release; }, [types.RECEIVE_TAG_NOTES_ERROR](state, error) { state.fetchError = error; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js index 9f8146997c1..33cb3ee06d0 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js @@ -53,4 +53,5 @@ export default ({ tagNotes: '', includeTagNotes: false, + existingRelease: null, }); diff --git a/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue index 7c2a575bf93..40787cf72da 100644 --- a/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue +++ b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue @@ -5,7 +5,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '../components/runner_header.vue'; import RunnerUpdateForm from '../components/runner_update_form.vue'; import { I18N_FETCH_ERROR } from '../constants'; -import runnerQuery from '../graphql/details/runner.query.graphql'; +import runnerFormQuery from '../graphql/edit/runner_form.query.graphql'; import { captureException } from '../sentry_utils'; export default { @@ -32,7 +32,7 @@ export default { }, apollo: { runner: { - query: runnerQuery, + query: runnerFormQuery, variables() { return { id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId), diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue index ef48d7799f8..c3f317b40b0 100644 --- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue +++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue @@ -10,7 +10,7 @@ import RunnerPauseButton from '../components/runner_pause_button.vue'; import RunnerHeader from '../components/runner_header.vue'; import RunnerDetails from '../components/runner_details.vue'; import { I18N_FETCH_ERROR } from '../constants'; -import runnerQuery from '../graphql/details/runner.query.graphql'; +import runnerQuery from '../graphql/show/runner.query.graphql'; import { captureException } from '../sentry_utils'; import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; diff --git a/app/assets/javascripts/runner/components/runner_jobs.vue b/app/assets/javascripts/runner/components/runner_jobs.vue index b25d92d049e..4eb1312b204 100644 --- a/app/assets/javascripts/runner/components/runner_jobs.vue +++ b/app/assets/javascripts/runner/components/runner_jobs.vue @@ -1,7 +1,7 @@ <script> import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { createAlert } from '~/flash'; -import runnerJobsQuery from '../graphql/details/runner_jobs.query.graphql'; +import runnerJobsQuery from '../graphql/show/runner_jobs.query.graphql'; import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants'; import { captureException } from '../sentry_utils'; import { getPaginationVariables } from '../utils'; diff --git a/app/assets/javascripts/runner/components/runner_projects.vue b/app/assets/javascripts/runner/components/runner_projects.vue index d080d34fdd3..daca718e2b5 100644 --- a/app/assets/javascripts/runner/components/runner_projects.vue +++ b/app/assets/javascripts/runner/components/runner_projects.vue @@ -2,7 +2,7 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { sprintf, formatNumber } from '~/locale'; import { createAlert } from '~/flash'; -import runnerProjectsQuery from '../graphql/details/runner_projects.query.graphql'; +import runnerProjectsQuery from '../graphql/show/runner_projects.query.graphql'; import { I18N_ASSIGNED_PROJECTS, I18N_NONE, diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue index a87976d0240..56c9007a781 100644 --- a/app/assets/javascripts/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -18,7 +18,7 @@ import { redirectTo } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import { captureException } from '~/runner/sentry_utils'; import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; -import runnerUpdateMutation from '../graphql/details/runner_update.mutation.graphql'; +import runnerUpdateMutation from '../graphql/edit/runner_update.mutation.graphql'; import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; export default { diff --git a/app/assets/javascripts/runner/graphql/details/runner.query.graphql b/app/assets/javascripts/runner/graphql/details/runner.query.graphql deleted file mode 100644 index df6ce19fd09..00000000000 --- a/app/assets/javascripts/runner/graphql/details/runner.query.graphql +++ /dev/null @@ -1,7 +0,0 @@ -#import "ee_else_ce/runner/graphql/details/runner_details.fragment.graphql" - -query getRunner($id: CiRunnerID!) { - runner(id: $id) { - ...RunnerDetails - } -} diff --git a/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql deleted file mode 100644 index 2449ee0fc0f..00000000000 --- a/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql +++ /dev/null @@ -1,5 +0,0 @@ -#import "./runner_details_shared.fragment.graphql" - -fragment RunnerDetails on CiRunner { - ...RunnerDetailsShared -} diff --git a/app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql deleted file mode 100644 index b79ad4d9280..00000000000 --- a/app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql +++ /dev/null @@ -1,39 +0,0 @@ -fragment RunnerDetailsShared on CiRunner { - __typename - id - shortSha - runnerType - active - accessLevel - runUntagged - locked - ipAddress - executorName - architectureName - platformName - description - maximumTimeout - jobCount - tagList - createdAt - status(legacyMode: null) - contactedAt - version - editAdminUrl - userPermissions { - updateRunner - deleteRunner - } - groups { - # Only a single group can be loaded here, while projects - # are loaded separately using the query with pagination - # parameters `runner_projects.query.graphql`. - nodes { - id - avatarUrl - name - fullName - webUrl - } - } -} diff --git a/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql b/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql new file mode 100644 index 00000000000..b732d587d70 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql @@ -0,0 +1,5 @@ +#import "./runner_fields_shared.fragment.graphql" + +fragment RunnerFields on CiRunner { + ...RunnerFieldsShared +} diff --git a/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql new file mode 100644 index 00000000000..f900a0450e5 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql @@ -0,0 +1,15 @@ +fragment RunnerFieldsShared on CiRunner { + __typename + id + shortSha + runnerType + active + accessLevel + runUntagged + locked + description + maximumTimeout + tagList + createdAt + status(legacyMode: null) +} diff --git a/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql b/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql new file mode 100644 index 00000000000..0bf66c223fc --- /dev/null +++ b/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql @@ -0,0 +1,7 @@ +#import "ee_else_ce/runner/graphql/edit/runner_fields.fragment.graphql" + +query getRunnerForm($id: CiRunnerID!) { + runner(id: $id) { + ...RunnerFields + } +} diff --git a/app/assets/javascripts/runner/graphql/details/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql index 352b95c1a39..8694a51b5a4 100644 --- a/app/assets/javascripts/runner/graphql/details/runner_update.mutation.graphql +++ b/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/runner/graphql/details/runner_details.fragment.graphql" +#import "ee_else_ce/runner/graphql/edit/runner_fields.fragment.graphql" # Mutation for updates from the runner form, loads # attributes shown in the runner details. @@ -6,7 +6,7 @@ mutation runnerUpdate($input: RunnerUpdateInput!) { runnerUpdate(input: $input) { runner { - ...RunnerDetails + ...RunnerFields } errors } diff --git a/app/assets/javascripts/runner/graphql/show/runner.query.graphql b/app/assets/javascripts/runner/graphql/show/runner.query.graphql new file mode 100644 index 00000000000..178816b58bd --- /dev/null +++ b/app/assets/javascripts/runner/graphql/show/runner.query.graphql @@ -0,0 +1,41 @@ +query getRunner($id: CiRunnerID!) { + runner(id: $id) { + __typename + id + shortSha + runnerType + active + accessLevel + runUntagged + locked + ipAddress + executorName + architectureName + platformName + description + maximumTimeout + jobCount + tagList + createdAt + status(legacyMode: null) + contactedAt + version + editAdminUrl + userPermissions { + updateRunner + deleteRunner + } + groups { + # Only a single group can be loaded here, while projects + # are loaded separately using the query with pagination + # parameters `runner_projects.query.graphql`. + nodes { + id + avatarUrl + name + fullName + webUrl + } + } + } +} diff --git a/app/assets/javascripts/runner/graphql/details/runner_jobs.query.graphql b/app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql index 14585e62bf2..14585e62bf2 100644 --- a/app/assets/javascripts/runner/graphql/details/runner_jobs.query.graphql +++ b/app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql diff --git a/app/assets/javascripts/runner/graphql/details/runner_projects.query.graphql b/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql index cb27de7c200..cb27de7c200 100644 --- a/app/assets/javascripts/runner/graphql/details/runner_projects.query.graphql +++ b/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index ddfd7e66bcb..dfa2ca2d20c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -199,7 +199,7 @@ export default { > <gl-button category="primary" - variant="success" + variant="confirm" :disabled="!Boolean(selectedProject)" class="gl-text-center! issuable-move-button" @click="handleMoveClick" diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index dfb9ef54f7c..5667617b610 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -238,6 +238,11 @@ class ProjectsController < Projects::ApplicationController edit_project_path(@project, anchor: 'js-export-project'), notice: _("Project export started. A download link will be sent by email and made available on this page.") ) + rescue Project::ExportLimitExceeded => ex + redirect_to( + edit_project_path(@project, anchor: 'js-export-project'), + alert: ex.to_s + ) end def download_export diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index aa0e8c55470..f6fff68e98b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -272,6 +272,7 @@ module ApplicationSettingsHelper :invisible_captcha_enabled, :max_artifacts_size, :max_attachment_size, + :max_export_size, :max_import_size, :max_pages_size, :max_yaml_size_bytes, diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 6eaeaf1b025..29b2f2eb12f 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -246,15 +246,15 @@ module MergeRequestsHelper '' end - link_to branch, branch_path, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-p-2' + link_to branch, branch_path, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-p-2 gl-display-inline-block gl-text-truncate gl-w-30p gl-mb-n3' end def merge_request_header(project, merge_request) link_to_author = link_to_member(project, merge_request.author, size: 24, extra_class: 'gl-font-weight-bold', avatar: false) copy_button = clipboard_button(text: merge_request.source_branch, title: _('Copy branch name'), class: 'btn btn-default btn-sm gl-button btn-default-tertiary btn-icon gl-display-none! gl-md-display-inline-block! js-source-branch-copy') - target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-p-2' + target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-p-2 gl-display-inline-block gl-text-truncate gl-w-20p gl-mb-n3' - _('%{author} requested to merge %{span_start}%{source_branch} %{copy_button}%{span_end} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe, span_start: '<span class="gl-display-inline-block">'.html_safe, span_end: '</span>'.html_safe } + _('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe } end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 0d514773891..20d0dd9b30c 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -53,13 +53,11 @@ module ProfilesHelper # Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip def ssh_key_expiration_tooltip(key) return key.errors.full_messages.join(', ') if key.errors.full_messages.any? - - s_('Profiles|Key usable beyond expiration date.') if key.expired? end # Overridden in EE::ProfilesHelper#ssh_key_expires_field_description def ssh_key_expires_field_description - s_('Profiles|Key can still be used after expiration.') + s_('Profiles|Key becomes invalid on this date.') end # Overridden in EE::ProfilesHelper#ssh_key_expiration_policy_enabled? diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a49658ce7e0..bf68101934b 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -13,6 +13,7 @@ class ApplicationSetting < ApplicationRecord ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22' ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18' ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22' + ignore_column :enforce_ssh_key_expiration, remove_with: '15.2', remove_after: '2022-07-22' ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22' INSTANCE_REVIEW_MIN_USERS = 50 @@ -201,6 +202,10 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :max_export_size, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_import_size, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 194356acc51..36366b375b9 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -108,6 +108,7 @@ module ApplicationSettingImplementation mailgun_events_enabled: false, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], + max_export_size: 0, max_import_size: 0, max_yaml_size_bytes: 1.megabyte, max_yaml_depth: 100, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5622f228d83..5bb9f8fcfe1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -757,7 +757,7 @@ module Ci end def valid_token?(token) - self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token) + self.token && token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token) end # acts_as_taggable uses this method create/remove tags with contexts diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb index 00e55d0fd89..49e8a04e251 100644 --- a/app/models/instance_configuration.rb +++ b/app/models/instance_configuration.rb @@ -47,6 +47,7 @@ class InstanceConfiguration { max_attachment_size: application_settings[:max_attachment_size].megabytes, receive_max_input_size: application_settings[:receive_max_input_size]&.megabytes, + max_export_size: application_settings[:max_export_size] > 0 ? application_settings[:max_export_size].megabytes : nil, max_import_size: application_settings[:max_import_size] > 0 ? application_settings[:max_import_size].megabytes : nil, diff_max_patch_bytes: application_settings[:diff_max_patch_bytes].bytes, max_artifacts_size: application_settings[:max_artifacts_size].megabytes, diff --git a/app/models/key.rb b/app/models/key.rb index 5e61ecf73f5..e093f9faad3 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -29,6 +29,7 @@ class Key < ApplicationRecord presence: { message: 'cannot be generated' } validate :key_meets_restrictions + validate :expiration, on: :create delegate :name, :email, to: :user, prefix: true @@ -148,6 +149,10 @@ class Key < ApplicationRecord "type is forbidden. Must be #{Gitlab::Utils.to_exclusive_sentence(allowed_types)}" end + + def expiration + errors.add(:key, message: 'has expired') if expired? + end end Key.prepend_mod_with('Key') diff --git a/app/models/project.rb b/app/models/project.rb index 528f87972b3..dd5bc9ded6a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -49,6 +49,7 @@ class Project < ApplicationRecord ignore_columns :container_registry_enabled, remove_after: '2021-09-22', remove_with: '14.4' BoardLimitExceeded = Class.new(StandardError) + ExportLimitExceeded = Class.new(StandardError) ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4' ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4' @@ -2054,6 +2055,8 @@ class Project < ApplicationRecord end def add_export_job(current_user:, after_export_strategy: nil, params: {}) + check_project_export_limit! + job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params) if job_id @@ -3112,6 +3115,14 @@ class Project < ApplicationRecord Projects::SyncEvent.enqueue_worker end end + + def check_project_export_limit! + return if Gitlab::CurrentSettings.current_application_settings.max_export_size == 0 + + if self.statistics.storage_size > Gitlab::CurrentSettings.current_application_settings.max_export_size.megabytes + raise ExportLimitExceeded, _('The project size exceeds the export limit.') + end + end end Project.prepend_mod_with('Project') diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb index adbe3ddfdad..2b81967b1e3 100644 --- a/app/services/merge_requests/push_options_handler_service.rb +++ b/app/services/merge_requests/push_options_handler_service.rb @@ -147,6 +147,10 @@ module MergeRequests params[:milestone] = milestone if milestone end + if params.key?(:description) + params[:description] = params[:description].gsub('\n', "\n") + end + params end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index d55efbaf701..f914de138a9 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -19,6 +19,10 @@ = f.label :receive_max_input_size, _('Maximum push size (MB)'), class: 'label-light' = f.number_field :receive_max_input_size, class: 'form-control gl-form-input qa-receive-max-input-size-field', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body' } .form-group + = f.label :max_export_size, _('Maximum export size (MB)'), class: 'label-light' + = f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' } + %span.form-text.text-muted= _('Set to 0 for no size limit.') + .form-group = f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light' = f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' } %span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.') @@ -30,7 +34,6 @@ = render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f = render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f = render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f - = render_if_exists 'admin/application_settings/enforce_ssh_key_expiration', form: f .form-group = f.label :user_oauth_applications, _('User OAuth applications'), class: 'label-bold' diff --git a/app/views/help/instance_configuration/_size_limits.html.haml b/app/views/help/instance_configuration/_size_limits.html.haml index b592eeed020..90501450385 100644 --- a/app/views/help/instance_configuration/_size_limits.html.haml +++ b/app/views/help/instance_configuration/_size_limits.html.haml @@ -24,6 +24,9 @@ %td= _('Maximum push size') %td= instance_configuration_human_size_cell(size_limits[:receive_max_input_size]) %tr + %td= _('Maximum export size') + %td= instance_configuration_human_size_cell(size_limits[:max_export_size]) + %tr %td= _('Maximum import size') %td= instance_configuration_human_size_cell(size_limits[:max_import_size]) %tr diff --git a/config/feature_flags/development/ci_authenticate_running_job_token_for_artifacts.yml b/config/feature_flags/development/ci_authenticate_running_job_token_for_artifacts.yml new file mode 100644 index 00000000000..653380f445e --- /dev/null +++ b/config/feature_flags/development/ci_authenticate_running_job_token_for_artifacts.yml @@ -0,0 +1,8 @@ +--- +name: ci_authenticate_running_job_token_for_artifacts +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83713 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357075 +milestone: '15.0' +type: development +group: group::pipeline insights +default_enabled: false diff --git a/config/feature_flags/development/ci_expose_running_job_token_for_artifacts.yml b/config/feature_flags/development/ci_expose_running_job_token_for_artifacts.yml new file mode 100644 index 00000000000..aa6bcbeb1d1 --- /dev/null +++ b/config/feature_flags/development/ci_expose_running_job_token_for_artifacts.yml @@ -0,0 +1,8 @@ +--- +name: ci_expose_running_job_token_for_artifacts +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83713 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357075 +milestone: '15.0' +type: development +group: group::pipeline insights +default_enabled: false diff --git a/data/deprecations/15-0-deprecate-postgresql-12.yml b/data/deprecations/15-0-deprecate-postgresql-12.yml new file mode 100644 index 00000000000..bebfba64405 --- /dev/null +++ b/data/deprecations/15-0-deprecate-postgresql-12.yml @@ -0,0 +1,19 @@ +- name: "PostgreSQL 12 deprecated" + announcement_milestone: "15.0" + announcement_date: "2022-05-22" + removal_milestone: "16.0" + removal_date: "2023-05-22" + breaking_change: true + reporter: iroussos + body: | # Do not modify this line, instead modify the lines below. + Support for PostgreSQL 12 is scheduled for removal in GitLab 16.0. + In GitLab 16.0, PostgreSQL 13 becomes the minimum required PostgreSQL version. + + PostgreSQL 12 will be supported for the full GitLab 15 release cycle. + PostgreSQL 13 will also be supported for instances that want to upgrade prior to GitLab 16.0. + + Upgrading to PostgreSQL 13 is not yet supported for GitLab instances with Geo enabled. Geo support for PostgreSQL 13 will be announced in a minor release version of GitLab 15, after the process is fully supported and validated. For more information, read the Geo related verifications on the [support epic for PostgreSQL 13](https://gitlab.com/groups/gitlab-org/-/epics/3832). + stage: Enablement + tiers: [Free, Premium, Ultimate] + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349185 + documentation_url: https://docs.gitlab.com/omnibus/development/managing-postgresql-versions.html diff --git a/db/migrate/20220426130217_add_max_export_size_to_application_settings.rb b/db/migrate/20220426130217_add_max_export_size_to_application_settings.rb new file mode 100644 index 00000000000..d1488a77d14 --- /dev/null +++ b/db/migrate/20220426130217_add_max_export_size_to_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddMaxExportSizeToApplicationSettings < Gitlab::Database::Migration[2.0] + def change + add_column :application_settings, :max_export_size, :integer, default: 0 + end +end diff --git a/db/post_migrate/20220505060011_remove_namespaces_id_parent_id_partial_index.rb b/db/post_migrate/20220505060011_remove_namespaces_id_parent_id_partial_index.rb new file mode 100644 index 00000000000..5125a97af7e --- /dev/null +++ b/db/post_migrate/20220505060011_remove_namespaces_id_parent_id_partial_index.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class RemoveNamespacesIdParentIdPartialIndex < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + NAME = 'index_namespaces_id_parent_id_is_null' + + def up + remove_concurrent_index :namespaces, :id, name: NAME + end + + def down + add_concurrent_index :namespaces, :id, where: 'parent_id IS NULL', name: NAME + end +end diff --git a/db/schema_migrations/20220426130217 b/db/schema_migrations/20220426130217 new file mode 100644 index 00000000000..d8df97c8516 --- /dev/null +++ b/db/schema_migrations/20220426130217 @@ -0,0 +1 @@ +5a55099d1f50c3059778e340bbbe519d4fcd6c1eefb235191f8db02f92b7b49e
\ No newline at end of file diff --git a/db/schema_migrations/20220505060011 b/db/schema_migrations/20220505060011 new file mode 100644 index 00000000000..dd31c727827 --- /dev/null +++ b/db/schema_migrations/20220505060011 @@ -0,0 +1 @@ +aa0e6f29d918bff13cbf499e465f63320dbb6ed5a6940c2c438fe015dcc7fcd6
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e4a24a866d4..3a955ccac67 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11292,6 +11292,7 @@ CREATE TABLE application_settings ( inactive_projects_send_warning_email_after_months integer DEFAULT 1 NOT NULL, delayed_group_deletion boolean DEFAULT true NOT NULL, arkose_labs_namespace text DEFAULT 'client'::text NOT NULL, + max_export_size integer DEFAULT 0, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), @@ -28385,8 +28386,6 @@ CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_stat CREATE INDEX index_namespaces_id_parent_id_is_not_null ON namespaces USING btree (id) WHERE (parent_id IS NOT NULL); -CREATE INDEX index_namespaces_id_parent_id_is_null ON namespaces USING btree (id) WHERE (parent_id IS NULL); - CREATE UNIQUE INDEX index_namespaces_name_parent_id_type ON namespaces USING btree (name, parent_id, type); CREATE INDEX index_namespaces_on_created_at ON namespaces USING btree (created_at); diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 9408e7c25a6..bb7317d04eb 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -26,6 +26,7 @@ The following API resources are available in the project context: | [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) | | [Access tokens](project_access_tokens.md) | `/projects/:id/access_tokens` (also available for groups) | | [Agents](cluster_agents.md) | `/projects/:id/cluster_agents` | +| [Agent Tokens](cluster_agent_tokens.md) | `/projects/:id/cluster_agents/:agent_id/tokens` | | [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` | | [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` | | [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | diff --git a/doc/api/cluster_agent_tokens.md b/doc/api/cluster_agent_tokens.md new file mode 100644 index 00000000000..92814e5e9af --- /dev/null +++ b/doc/api/cluster_agent_tokens.md @@ -0,0 +1,216 @@ +--- +stage: Configure +group: Configure +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Agent Tokens API **(FREE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/347046) in GitLab 15.0. + +Use the Agent Tokens API to manage tokens for the GitLab agent for Kubernetes. + +## List tokens for an agent + +Returns a list of tokens for an agent. + +You must have at least the Developer role to use this endpoint. + +```plaintext +GET /projects/:id/cluster_agents/:agent_id/tokens +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|------------|-------------------|-----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user. | +| `agent_id` | integer or string | yes | ID of the agent. | + +Response: + +The response is a list of tokens with the following fields: + +| Attribute | Type | Description | +|----------------------|----------------|-------------------------------------------------------------------| +| `id` | integer | ID of the token. | +| `name` | string | Name of the token. | +| `description` | string or null | Description of the token. | +| `agent_id` | integer | ID of the agent the token belongs to. | +| `status` | string | The status of the token. Valid values are `active` and `revoked`. | +| `created_at` | string | ISO8601 datetime when the token was created. | +| `created_by_user_id` | string | User ID of the user who created the token. | + +Example request: + +```shell +curl --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/projects/20/cluster_agents/5/tokens" +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "abcd", + "description": "Some token", + "agent_id": 5, + "status": "active", + "created_at": "2022-03-25T14:12:11.497Z", + "created_by_user_id": 1 + }, + { + "id": 2, + "name": "foobar", + "description": null, + "agent_id": 5, + "status": "active", + "created_at": "2022-03-25T14:12:11.497Z", + "created_by_user_id": 1 + } +] +``` + +NOTE: +The `last_used_at` field for a token is only returned when getting a single agent token. + +## Get a single agent token + +Gets a single agent token. + +You must have at least the Developer role to use this endpoint. + +```shell +GET /projects/:id/cluster_agents/:agent_id/tokens/:token_id +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|------------|-------------------|----------|-------------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user. | +| `agent_id` | integer | yes | ID of the agent. | +| `token_id` | integer | yes | ID of the token. | + +Response: + +The response is a single token with the following fields: + +| Attribute | Type | Description | +|----------------------|----------------|-------------------------------------------------------------------| +| `id` | integer | ID of the token. | +| `name` | string | Name of the token. | +| `description` | string or null | Description of the token. | +| `agent_id` | integer | ID of the agent the token belongs to. | +| `status` | string | The status of the token. Valid values are `active` and `revoked`. | +| `created_at` | string | ISO8601 datetime when the token was created. | +| `created_by_user_id` | string | User ID of the user who created the token. | +| `last_used_at` | string or null | ISO8601 datetime when the token was last used. | + +Example request: + +```shell +curl --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/projects/20/cluster_agents/5/token/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "abcd", + "description": "Some token", + "agent_id": 5, + "status": "active", + "created_at": "2022-03-25T14:12:11.497Z", + "created_by_user_id": 1, + "last_used_at": null +} +``` + +## Create an agent token + +Creates a new token for an agent. + +You must have at least the Maintainer role to use this endpoint. + +```shell +POST /projects/:id/cluster_agents/:agent_id/tokens +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|---------------|-------------------|----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user. | +| `agent_id` | integer | yes | ID of the agent. | +| `name` | string | yes | Name for the token. | +| `description` | string | no | Description for the token. | + +Response: + +The response is the new token with the following fields: + +| Attribute | Type | Description | +|----------------------|----------------|-------------------------------------------------------------------| +| `id` | integer | ID of the token. | +| `name` | string | Name of the token. | +| `description` | string or null | Description of the token. | +| `agent_id` | integer | ID of the agent the token belongs to. | +| `status` | string | The status of the token. Valid values are `active` and `revoked`. | +| `created_at` | string | ISO8601 datetime when the token was created. | +| `created_by_user_id` | string | User ID of the user who created the token. | +| `last_used_at` | string or null | ISO8601 datetime when the token was last used. | +| `token` | string | The secret token value. | + +NOTE: +The `token` is only returned in the response of the `POST` endpoint and cannot be retrieved afterwards. + +Example request: + +```shell +curl --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/projects/20/cluster_agents/5/tokens" \ + -H "Content-Type:application/json" \ + -X POST --data '{"name":"some-token"}' +``` + +Example response: + +```json +{ + "id": 1, + "name": "abcd", + "description": "Some token", + "agent_id": 5, + "status": "active", + "created_at": "2022-03-25T14:12:11.497Z", + "created_by_user_id": 1, + "last_used_at": null, + "token": "qeY8UVRisx9y3Loxo1scLxFuRxYcgeX3sxsdrpP_fR3Loq4xyg" +} +``` + +## Revoke an agent token + +Revokes an agent token. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +DELETE /projects/:id/cluster_agents/:agent_id/tokens/:token_id +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|------------|-------------------|----------|---------------------------------------------------------------------------------------------------------------- -| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user. | +| `agent_id` | integer | yes | ID of the agent. | +| `token_id` | integer | yes | ID of the token. | + +Example request: + +```shell +curl --request DELETE --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/projects/20/cluster_agents/5/tokens/1 +``` diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index bc8feb29e8e..df16eb54132 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9462,7 +9462,7 @@ Represents the total number of issues and their weights for a particular day. | <a id="ciminutesnamespacemonthlyusageminutes"></a>`minutes` | [`Int`](#int) | Total number of minutes used by all projects in the namespace. | | <a id="ciminutesnamespacemonthlyusagemonth"></a>`month` | [`String`](#string) | Month related to the usage data. | | <a id="ciminutesnamespacemonthlyusagemonthiso8601"></a>`monthIso8601` | [`ISO8601Date`](#iso8601date) | Month related to the usage data in ISO 8601 date format. | -| <a id="ciminutesnamespacemonthlyusageprojects"></a>`projects` | [`CiMinutesProjectMonthlyUsageConnection`](#ciminutesprojectmonthlyusageconnection) | CI minutes usage data for projects in the namespace. (see [Connections](#connections)) | +| <a id="ciminutesnamespacemonthlyusageprojects"></a>`projects` | [`CiMinutesProjectMonthlyUsageConnection`](#ciminutesprojectmonthlyusageconnection) | CI/CD minutes usage data for projects in the namespace. (see [Connections](#connections)) | | <a id="ciminutesnamespacemonthlyusagesharedrunnersduration"></a>`sharedRunnersDuration` | [`Int`](#int) | Total duration (in seconds) of shared runners use by the namespace for the month. | ### `CiMinutesProjectMonthlyUsage` @@ -9471,7 +9471,7 @@ Represents the total number of issues and their weights for a particular day. | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="ciminutesprojectmonthlyusageminutes"></a>`minutes` | [`Int`](#int) | Number of CI minutes used by the project in the month. | +| <a id="ciminutesprojectmonthlyusageminutes"></a>`minutes` | [`Int`](#int) | Number of CI/CD minutes used by the project in the month. | | <a id="ciminutesprojectmonthlyusagename"></a>`name` | [`String`](#string) | Name of the project. | | <a id="ciminutesprojectmonthlyusagesharedrunnersduration"></a>`sharedRunnersDuration` | [`Int`](#int) | Total duration (in seconds) of shared runners use by the project for the month. | diff --git a/doc/api/managed_licenses.md b/doc/api/managed_licenses.md index f2626574bf0..31f1cb41eb3 100644 --- a/doc/api/managed_licenses.md +++ b/doc/api/managed_licenses.md @@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Managed Licenses API **(ULTIMATE)** WARNING: -"approval" and "blacklisted" approval statuses are deprecated and scheduled to be changed to "allowed" and "denied" in GitLab 15.0. +"approval" and "blacklisted" approval statuses are changed to "allowed" and "denied" in GitLab 15.0. ## List managed licenses @@ -32,12 +32,12 @@ Example response: { "id": 1, "name": "MIT", - "approval_status": "approved" + "approval_status": "allowed" }, { "id": 3, "name": "ISC", - "approval_status": "blacklisted" + "approval_status": "denied" } ] ``` @@ -65,7 +65,7 @@ Example response: { "id": 1, "name": "MIT", - "approval_status": "blacklisted" + "approval_status": "denied" } ``` @@ -81,7 +81,7 @@ POST /projects/:id/managed_licenses | ------------- | ------- | -------- | ---------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | yes | The name of the managed license | -| `approval_status` | string | yes | The approval status of the license. "allowed" or "denied". "blacklisted" and "approved" are deprecated. | +| `approval_status` | string | yes | The approval status of the license. "allowed" or "denied". | ```shell curl --data "name=MIT&approval_status=denied" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses" @@ -93,7 +93,7 @@ Example response: { "id": 1, "name": "MIT", - "approval_status": "approved" + "approval_status": "allowed" } ``` @@ -128,7 +128,7 @@ PATCH /projects/:id/managed_licenses/:managed_license_id | --------------- | ------- | --------------------------------- | ------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | | `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project | -| `approval_status` | string | yes | The approval status of the license. "allowed" or "denied". "blacklisted" and "approved" are deprecated. | +| `approval_status` | string | yes | The approval status of the license. "allowed" or "denied". | ```shell curl --request PATCH --data "approval_status=denied" \ @@ -141,6 +141,6 @@ Example response: { "id": 1, "name": "MIT", - "approval_status": "blacklisted" + "approval_status": "denied" } ``` diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index 3b35a43398e..3f2cc09aa1e 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -49,6 +49,12 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla NOTE: The upload request is sent with `Content-Type: application/gzip` header. Ensure that your pre-signed URL includes this as part of the signature. +NOTE: +As an administrator, you can modify the maximum export file size. By default, +it is set to `0`, for unlimited. To change this value, edit `max_export_size` +in the [Application settings API](settings.md#change-application-settings) +or the [Admin UI](../user/admin_area/settings/account_and_limit_settings.md). + ## Export status Get the status of export. diff --git a/doc/api/settings.md b/doc/api/settings.md index cf20cd279fc..a55ce223084 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -36,6 +36,7 @@ Example response: "password_authentication_enabled_for_web" : true, "after_sign_out_path" : null, "max_attachment_size" : 10, + "max_export_size": 50, "max_import_size": 50, "user_oauth_applications" : true, "updated_at" : "2016-01-04T15:44:55.176Z", @@ -147,6 +148,7 @@ Example response: "default_branch_protection": 2, "restricted_visibility_levels": [], "max_attachment_size": 10, + "max_export_size": 50, "max_import_size": 50, "session_expire_delay": 10080, "default_ci_config_path" : null, @@ -367,6 +369,7 @@ listed in the descriptions of the relevant settings. | `maintenance_mode` **(PREMIUM)** | boolean | no | When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. | | `max_artifacts_size` | integer | no | Maximum artifacts size in MB. | | `max_attachment_size` | integer | no | Limit attachment size in MB. | +| `max_export_size` | integer | no | Maximum export size in MB. 0 for unlimited. Default = 0 (unlimited). | | `max_import_size` | integer | no | Maximum import size in MB. 0 for unlimited. Default = 0 (unlimited) [Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8. | | `max_pages_size` | integer | no | Maximum size of pages repositories in MB. | | `max_personal_access_token_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for access tokens in days. | diff --git a/doc/development/merge_request_application_and_rate_limit_guidelines.md b/doc/development/merge_request_application_and_rate_limit_guidelines.md index 94ae126802a..62bf62f6275 100644 --- a/doc/development/merge_request_application_and_rate_limit_guidelines.md +++ b/doc/development/merge_request_application_and_rate_limit_guidelines.md @@ -14,7 +14,7 @@ Every new feature should have safe usage limits included in its implementation. Limits are applicable for: - System-level resource pools such as API requests, SSHD connections, database connections, storage, and so on. -- Domain-level objects such as CI minutes, groups, sign-in attempts, and so on. +- Domain-level objects such as CI/CD minutes, groups, sign-in attempts, and so on. ## When limits are required diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 6dfd8202ac9..d26e20a42b9 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -127,9 +127,9 @@ scripts/regenerate-schema TARGET=12-9-stable-ee scripts/regenerate-schema ``` -There may be times when the `scripts/regenerate-schema` script creates -additional differences. In this case, a manual procedure can be used, -where <migration ID> is the DATETIME part of the migration file. +The `scripts/regenerate-schema` script can create additional differences. +If this happens, use a manual procedure where `<migration ID>` is the `DATETIME` +part of the migration file. ```shell # Rebase against master diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index cb1e224f062..8b5fd3952ba 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -93,7 +93,7 @@ In addition, there are a few circumstances where we would always run the full Je ### Fork pipelines We only run the minimal RSpec & Jest jobs for fork pipelines unless the `pipeline:run-all-rspec` -label is set on the MR. The goal is to reduce the CI minutes consumed by fork pipelines. +label is set on the MR. The goal is to reduce the CI/CD minutes consumed by fork pipelines. See the [experiment issue](https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1170). diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index fea778625ea..2e104ea79d9 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -69,6 +69,27 @@ be present during the 16.x cycle to avoid breaking the API signature, and will b **Planned removal milestone: <span class="removal-milestone">16.0</span> (2023-05-22)** </div> +<div class="deprecation removal-160 breaking-change"> + +### PostgreSQL 12 deprecated + +WARNING: +This feature will be changed or removed in 16.0 +as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Before updating GitLab, review the details carefully to determine if you need to make any +changes to your code, settings, or workflow. + +Support for PostgreSQL 12 is scheduled for removal in GitLab 16.0. +In GitLab 16.0, PostgreSQL 13 becomes the minimum required PostgreSQL version. + +PostgreSQL 12 will be supported for the full GitLab 15 release cycle. +PostgreSQL 13 will also be supported for instances that want to upgrade prior to GitLab 16.0. + +Upgrading to PostgreSQL 13 is not yet supported for GitLab instances with Geo enabled. Geo support for PostgreSQL 13 will be announced in a minor release version of GitLab 15, after the process is fully supported and validated. For more information, read the Geo related verifications on the [support epic for PostgreSQL 13](https://gitlab.com/groups/gitlab-org/-/epics/3832). + +**Planned removal milestone: <span class="removal-milestone">16.0</span> (2023-05-22)** +</div> + <div class="deprecation removal-152"> ### Vulnerability Report sort by State diff --git a/doc/update/index.md b/doc/update/index.md index 19fe0b1b84e..7fe1d67168b 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -406,7 +406,8 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap ### 14.10.0 -- The upgrade to GitLab 14.10 executes a [concurrent index drop](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84308) of unneeded +- Before upgrading to GitLab 14.10, you need to already have the latest 14.9.Z installed on your instance. + The upgrade to GitLab 14.10 executes a [concurrent index drop](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84308) of unneeded entries from the `ci_job_artifacts` database table. This could potentially run for multiple minutes, especially if the table has a lot of traffic and the migration is unable to acquire a lock. It is advised to let this process finish as restarting may result in data loss. diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 9b1a32e9083..fe713baeb39 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -66,6 +66,16 @@ because the [web server](../../../development/architecture.md#components) must receive the file before GitLab can generate the commit. Use [Git LFS](../../../topics/git/lfs/index.md) to add large files to a repository. +## Max export size + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86124) in GitLab 15.0. + +To modify the maximum file size for exports in GitLab: + +1. On the top bar, select **Menu > Admin**. +1. On the left sidebar, select **Settings > General**, then expand **Account and limit**. +1. Increase or decrease by changing the value in **Maximum export size (MB)**. + ## Max import size > [Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50 MB to unlimited in GitLab 13.8. @@ -231,25 +241,17 @@ Once a lifetime for SSH keys is set, GitLab: NOTE: When a user's SSH key becomes invalid they can delete and re-add the same key again. -## Allow expired SSH keys to be used (DEPRECATED) **(ULTIMATE SELF)** +<!--- start_remove The following content will be removed on remove_date: '2022-08-22' --> +## Allow expired SSH keys to be used (removed) **(ULTIMATE SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250480) in GitLab 13.9. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/320970) in GitLab 14.0. > - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351963) in GitLab 14.8. +> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351963) in GitLab 15.0. -WARNING: This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351963) in GitLab 14.8. - -By default, expired SSH keys **are not usable**. - -To allow the use of expired SSH keys: - -1. On the top bar, select **Menu > Admin**. -1. On the left sidebar, select **Settings > General**. -1. Expand the **Account and limit** section. -1. Uncheck the **Enforce SSH key expiration** checkbox. - -Disabling SSH key expiration immediately enables all expired SSH keys. +This feature was [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351963) in GitLab 15.0. +<!--- end_remove --> ## Limit the lifetime of access tokens **(ULTIMATE SELF)** diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 5e31cca51bd..62854810bdb 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -580,7 +580,7 @@ The following variables allow configuration of global dependency scanning settin | ----------------------------|------------ | | `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs to trust. The bundle of certificates provided here is also used by other tools during the scanning process, such as `git`, `yarn`, or `npm`. See [Using a custom SSL CA certificate authority](#using-a-custom-ssl-ca-certificate-authority) for more details. | | `DS_EXCLUDED_ANALYZERS` | Specify the analyzers (by name) to exclude from Dependency Scanning. For more information, see [Dependency Scanning Analyzers](analyzers.md). | -| `DS_DEFAULT_ANALYZERS` | ([**DEPRECATED - use `DS_EXCLUDED_ANALYZERS` instead**](https://gitlab.com/gitlab-org/gitlab/-/issues/287691)) Override the names of the official default images. For more information, see [Dependency Scanning Analyzers](analyzers.md). | +| `DS_DEFAULT_ANALYZERS` | This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351963) in GitLab 14.8 and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351963) in 15.0. Use `DS_EXCLUDED_ANALYZERS` instead. | | `DS_EXCLUDED_PATHS` | Exclude files and directories from the scan based on the paths. A comma-separated list of patterns. Patterns can be globs, or file or folder paths (for example, `doc,spec`). Parent directories also match patterns. Default: `"spec, test, tests, tmp"`. | | `DS_IMAGE_SUFFIX` | Suffix added to the image name. If set to `-fips`, `FIPS-enabled` images are used for scan. See [FIPS-enabled images](#fips-enabled-images) for more details. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354796) in GitLab 14.10. | | `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). | diff --git a/doc/user/ssh.md b/doc/user/ssh.md index 41f2d294e57..27bb7124afe 100644 --- a/doc/user/ssh.md +++ b/doc/user/ssh.md @@ -294,8 +294,6 @@ To use SSH with GitLab, copy your public key to your GitLab account: - GitLab 13.12 and earlier, the expiration date is informational only. It doesn't prevent you from using the key. Administrators can view expiration dates and use them for guidance when [deleting keys](admin_area/credentials_inventory.md#delete-a-users-ssh-key). - - GitLab 14.0 and later, the expiration date is enforced. Administrators can - [allow expired keys to be used](admin_area/settings/account_and_limit_settings.md#allow-expired-ssh-keys-to-be-used-deprecated). - GitLab checks all SSH keys at 02:00 AM UTC every day. It emails an expiration notice for all SSH keys that expire on the current date. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.) - GitLab checks all SSH keys at 01:00 AM UTC every day. It emails an expiration notice for all SSH keys that are scheduled to expire seven days from now. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.) 1. Select **Add key**. diff --git a/lib/api/api.rb b/lib/api/api.rb index 8a7333ffed2..65e784fe6d6 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -184,6 +184,7 @@ module API mount ::API::Ci::Triggers mount ::API::Ci::Variables mount ::API::Clusters::Agents + mount ::API::Clusters::AgentTokens mount ::API::CommitStatuses mount ::API::Commits mount ::API::ComposerPackages diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb index 44f9ebcf6c6..72e36d95dc5 100644 --- a/lib/api/ci/helpers/runner.rb +++ b/lib/api/ci/helpers/runner.rb @@ -53,7 +53,7 @@ module API # https://gitlab.com/gitlab-org/gitlab/-/issues/327703 forbidden! unless job - forbidden! unless job_token_valid?(job) + forbidden! unless job.valid_token?(job_token) forbidden!('Project has been deleted!') if job.project.nil? || job.project.pending_delete? forbidden!('Job has been erased!') if job.erased? @@ -77,6 +77,12 @@ module API job end + def authenticate_job_via_dependent_job! + forbidden! unless current_authenticated_job + forbidden! unless current_job + forbidden! unless can?(current_authenticated_job.user, :read_build, current_job) + end + def current_job id = params[:id] @@ -91,9 +97,28 @@ module API end end - def job_token_valid?(job) - token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s - token && job.valid_token?(token) + # TODO: Replace this with `#current_authenticated_job from API::Helpers` + # after the feature flag `ci_authenticate_running_job_token_for_artifacts` + # is removed. + # + # For the time being, this needs to be overridden because the API + # GET api/v4/jobs/:id/artifacts + # needs to allow requests using token whose job is not running. + # + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83713#note_942368526 + def current_authenticated_job + strong_memoize(:current_authenticated_job) do + ::Ci::AuthJobFinder.new(token: job_token).execute + end + end + + # The token used by runner to authenticate a request. + # In most cases, the runner uses the token belonging to the requested job. + # However, when requesting for job artifacts, the runner would use + # the token that belongs to downstream jobs that depend on the job that owns + # the artifacts. + def job_token + @job_token ||= (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s end def job_forbidden!(job, reason) @@ -120,6 +145,10 @@ module API def get_runner_config_from_request { config: attributes_for_keys(%w(gpus), params.dig('info', 'config')) } end + + def request_using_running_job_token? + current_job.present? && current_authenticated_job.present? && current_job != current_authenticated_job + end end end end diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 095f46e6885..72ba39861a9 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -324,9 +324,14 @@ module API optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts) end get '/:id/artifacts', feature_category: :build_artifacts do - job = authenticate_job!(require_running: false) + if Feature.enabled?(:ci_authenticate_running_job_token_for_artifacts, current_job&.project) && + request_using_running_job_token? + authenticate_job_via_dependent_job! + else + authenticate_job!(require_running: false) + end - present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download]) + present_carrierwave_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download]) end end end diff --git a/lib/api/clusters/agent_tokens.rb b/lib/api/clusters/agent_tokens.rb new file mode 100644 index 00000000000..1e52790f26b --- /dev/null +++ b/lib/api/clusters/agent_tokens.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module API + module Clusters + class AgentTokens < ::API::Base + include PaginationParams + + before { authenticate! } + + feature_category :kubernetes_management + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + params do + requires :agent_id, type: Integer, desc: 'The ID of an agent' + end + resource ':id/cluster_agents/:agent_id' do + resource :tokens do + desc 'List agent tokens' do + detail 'This feature was introduced in GitLab 15.0.' + success Entities::Clusters::AgentTokenBasic + end + params do + use :pagination + end + get do + authorize! :read_cluster, user_project + + agent = user_project.cluster_agents.find(params[:agent_id]) + + present paginate(agent.agent_tokens), with: Entities::Clusters::AgentTokenBasic + end + + desc 'Get a single agent token' do + detail 'This feature was introduced in GitLab 15.0.' + success Entities::Clusters::AgentToken + end + params do + requires :token_id, type: Integer, desc: 'The ID of the agent token' + end + get ':token_id' do + authorize! :read_cluster, user_project + + agent = user_project.cluster_agents.find(params[:agent_id]) + token = agent.agent_tokens.find(params[:token_id]) + + present token, with: Entities::Clusters::AgentToken + end + + desc 'Create an agent token' do + detail 'This feature was introduced in GitLab 15.0.' + success Entities::Clusters::AgentTokenWithToken + end + params do + requires :name, type: String, desc: 'The name for the token' + optional :description, type: String, desc: 'The description for the token' + end + post do + authorize! :create_cluster, user_project + + token_params = declared_params(include_missing: false) + + agent = user_project.cluster_agents.find(params[:agent_id]) + + result = ::Clusters::AgentTokens::CreateService.new( + container: agent.project, current_user: current_user, params: token_params.merge(agent_id: agent.id) + ).execute + + bad_request!(result[:message]) if result[:status] == :error + + present result[:token], with: Entities::Clusters::AgentTokenWithToken + end + + desc 'Revoke an agent token' do + detail 'This feature was introduced in GitLab 15.0.' + end + params do + requires :token_id, type: Integer, desc: 'The ID of the agent token' + end + delete ':token_id' do + authorize! :admin_cluster, user_project + + agent = user_project.cluster_agents.find(params[:agent_id]) + token = agent.agent_tokens.find(params[:token_id]) + + # Skipping explicit error handling and relying on exceptions + token.revoked! + + status :no_content + end + end + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/dependency.rb b/lib/api/entities/ci/job_request/dependency.rb index 2672a4a245b..791bfba1079 100644 --- a/lib/api/entities/ci/job_request/dependency.rb +++ b/lib/api/entities/ci/job_request/dependency.rb @@ -5,7 +5,16 @@ module API module Ci module JobRequest class Dependency < Grape::Entity - expose :id, :name, :token + expose :id, :name + + expose :token do |job, options| + if ::Feature.enabled?(:ci_expose_running_job_token_for_artifacts, job.project) + options[:running_job]&.token + else + job.token + end + end + expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.available_artifacts? } end end diff --git a/lib/api/entities/ci/job_request/response.rb b/lib/api/entities/ci/job_request/response.rb index 86c945cb236..9de415ebacb 100644 --- a/lib/api/entities/ci/job_request/response.rb +++ b/lib/api/entities/ci/job_request/response.rb @@ -28,8 +28,10 @@ module API expose :artifacts, using: Entities::Ci::JobRequest::Artifacts expose :cache, using: Entities::Ci::JobRequest::Cache expose :credentials, using: Entities::Ci::JobRequest::Credentials - expose :all_dependencies, as: :dependencies, using: Entities::Ci::JobRequest::Dependency expose :features + expose :dependencies do |job, options| + Entities::Ci::JobRequest::Dependency.represent(job.all_dependencies, options.merge(running_job: job)) + end end end end diff --git a/lib/api/entities/clusters/agent_token.rb b/lib/api/entities/clusters/agent_token.rb new file mode 100644 index 00000000000..e8cc1009361 --- /dev/null +++ b/lib/api/entities/clusters/agent_token.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Clusters + class AgentToken < AgentTokenBasic + expose :last_used_at + end + end + end +end diff --git a/lib/api/entities/clusters/agent_token_basic.rb b/lib/api/entities/clusters/agent_token_basic.rb new file mode 100644 index 00000000000..793ec8188b7 --- /dev/null +++ b/lib/api/entities/clusters/agent_token_basic.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + module Clusters + class AgentTokenBasic < Grape::Entity + expose :id + expose :name + expose :description + expose :agent_id + expose :status + expose :created_at + expose :created_by_user_id + end + end + end +end diff --git a/lib/api/entities/clusters/agent_token_with_token.rb b/lib/api/entities/clusters/agent_token_with_token.rb new file mode 100644 index 00000000000..8b84c80795f --- /dev/null +++ b/lib/api/entities/clusters/agent_token_with_token.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Clusters + class AgentTokenWithToken < AgentToken + expose :token + end + end + end +end diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index e7d34c75b05..d610b5e4f95 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -66,9 +66,13 @@ module API if export_strategy&.invalid? render_validation_error!(export_strategy) else - user_project.add_export_job(current_user: current_user, - after_export_strategy: export_strategy, - params: project_export_params) + begin + user_project.add_export_job(current_user: current_user, + after_export_strategy: export_strategy, + params: project_export_params) + rescue Project::ExportLimitExceeded => e + render_api_error!(e.message, 400) + end end accepted! diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 774ab472f2d..62f4aeb038a 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -95,6 +95,7 @@ module API optional :invisible_captcha_enabled, type: Boolean, desc: 'Enable Invisible Captcha spam detection during signup.' optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' + optional :max_export_size, type: Integer, desc: 'Maximum export size in MB' optional :max_import_size, type: Integer, desc: 'Maximum import size in MB' optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.' diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index c5f0a9ca91e..6e81a578d4a 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -40,7 +40,7 @@ module API desc 'Get a list of all metric definitions' do detail 'This feature was introduced in GitLab 13.11.' end - get 'metric_definitions' do + get 'metric_definitions', urgency: :low do content_type 'application/yaml' env['api.format'] = :binary diff --git a/lib/api/usage_data_non_sql_metrics.rb b/lib/api/usage_data_non_sql_metrics.rb index c764a942f5f..41f369a43b8 100644 --- a/lib/api/usage_data_non_sql_metrics.rb +++ b/lib/api/usage_data_non_sql_metrics.rb @@ -5,6 +5,7 @@ module API before { authenticated_as_admin! } feature_category :service_ping + urgency :low namespace 'usage_data' do before do diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb index a34519799d5..521fd7bf4cc 100644 --- a/lib/banzai/filter/references/abstract_reference_filter.rb +++ b/lib/banzai/filter/references/abstract_reference_filter.rb @@ -206,6 +206,7 @@ module Banzai link_content: !!link_content, link_reference: link_reference) data_attributes[:reference_format] = matches[:format] if matches.names.include?("format") + data_attributes.merge!(additional_object_attributes(object)) data = data_attribute(data_attributes) @@ -294,6 +295,10 @@ module Banzai placeholder_data[Regexp.last_match(1).to_i] end end + + def additional_object_attributes(object) + {} + end end end end diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb index 1053501de7b..337075b7ff8 100644 --- a/lib/banzai/filter/references/issue_reference_filter.rb +++ b/lib/banzai/filter/references/issue_reference_filter.rb @@ -31,6 +31,10 @@ module Banzai private + def additional_object_attributes(issue) + { issue_type: issue.issue_type } + end + def issue_path(issue, project) Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid) end diff --git a/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml index d41182ec9be..5b3baebd6fb 100644 --- a/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml @@ -12,7 +12,6 @@ variables: # Setting this variable will affect all Security templates # (SAST, Dependency Scanning, ...) SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products" - DS_DEFAULT_ANALYZERS: "bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python" DS_EXCLUDED_ANALYZERS: "" DS_EXCLUDED_PATHS: "spec, test, tests, tmp" DS_MAJOR_VERSION: 2 @@ -65,8 +64,7 @@ gemnasium-dependency_scanning: - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium([^-]|$)/ when: never - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && - $DS_DEFAULT_ANALYZERS =~ /gemnasium([^-]|$)/ + $GITLAB_FEATURES =~ /\bdependency_scanning\b/ exists: - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' - '{composer.lock,*/composer.lock,*/*/composer.lock}' @@ -93,8 +91,7 @@ gemnasium-maven-dependency_scanning: - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium-maven/ when: never - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && - $DS_DEFAULT_ANALYZERS =~ /gemnasium-maven/ + $GITLAB_FEATURES =~ /\bdependency_scanning\b/ exists: - '{build.gradle,*/build.gradle,*/*/build.gradle}' - '{build.gradle.kts,*/build.gradle.kts,*/*/build.gradle.kts}' @@ -116,8 +113,7 @@ gemnasium-python-dependency_scanning: - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium-python/ when: never - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && - $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ + $GITLAB_FEATURES =~ /\bdependency_scanning\b/ exists: - '{requirements.txt,*/requirements.txt,*/*/requirements.txt}' - '{requirements.pip,*/requirements.pip,*/*/requirements.pip}' @@ -128,7 +124,6 @@ gemnasium-python-dependency_scanning: # See https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && - $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ && $PIP_REQUIREMENTS_FILE bundler-audit-dependency_scanning: @@ -141,8 +136,7 @@ bundler-audit-dependency_scanning: - if: $DS_EXCLUDED_ANALYZERS =~ /bundler-audit/ when: never - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && - $DS_DEFAULT_ANALYZERS =~ /bundler-audit/ + $GITLAB_FEATURES =~ /\bdependency_scanning\b/ exists: - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' @@ -156,7 +150,6 @@ retire-js-dependency_scanning: - if: $DS_EXCLUDED_ANALYZERS =~ /retire.js/ when: never - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && - $DS_DEFAULT_ANALYZERS =~ /retire.js/ + $GITLAB_FEATURES =~ /\bdependency_scanning\b/ exists: - '{package.json,*/package.json,*/*/package.json}' diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index f98fb66ad21..cba63b3c6c7 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -177,8 +177,10 @@ module Gitlab def check_valid_actor! return unless key? - unless actor.valid? + if !actor.valid? raise ForbiddenError, "Your SSH key #{actor.errors[:key].first}." + elsif actor.expired? + raise ForbiddenError, "Your SSH key has expired." end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 48675b765a6..0c54855ebf7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -454,7 +454,7 @@ msgstr "" msgid "%{authorsName}'s thread" msgstr "" -msgid "%{author} requested to merge %{span_start}%{source_branch} %{copy_button}%{span_end} into %{target_branch} %{created_at}" +msgid "%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}" msgstr "" msgid "%{board_target} not found" @@ -13960,9 +13960,6 @@ msgstr "" msgid "Ends: %{endsAt}" msgstr "" -msgid "Enforce SSH key expiration" -msgstr "" - msgid "Enforce two-factor authentication" msgstr "" @@ -23312,6 +23309,12 @@ msgstr "" msgid "Maximum duration of a session." msgstr "" +msgid "Maximum export size" +msgstr "" + +msgid "Maximum export size (MB)" +msgstr "" + msgid "Maximum field length" msgstr "" @@ -23432,6 +23435,9 @@ msgstr "" msgid "Maximum size of Elasticsearch bulk indexing requests." msgstr "" +msgid "Maximum size of export files." +msgstr "" + msgid "Maximum size of import files." msgstr "" @@ -28807,9 +28813,6 @@ msgstr "" msgid "Profiles|Expiration date" msgstr "" -msgid "Profiles|Expired key is not valid." -msgstr "" - msgid "Profiles|Expired:" msgstr "" @@ -28837,9 +28840,6 @@ msgstr "" msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)" msgstr "" -msgid "Profiles|Invalid key." -msgstr "" - msgid "Profiles|Invalid password" msgstr "" @@ -28858,15 +28858,9 @@ msgstr "" msgid "Profiles|Key becomes invalid on this date. Maximum lifetime for SSH keys is %{max_ssh_key_lifetime} days" msgstr "" -msgid "Profiles|Key can still be used after expiration." -msgstr "" - msgid "Profiles|Key titles are publicly visible." msgstr "" -msgid "Profiles|Key usable beyond expiration date." -msgstr "" - msgid "Profiles|Last used:" msgstr "" @@ -31196,10 +31190,10 @@ msgstr "" msgid "Release|Something went wrong while getting the release details." msgstr "" -msgid "Release|Something went wrong while getting the tag notes." +msgid "Release|Something went wrong while saving the release details." msgstr "" -msgid "Release|Something went wrong while saving the release details." +msgid "Release|Unable to fetch the tag notes." msgstr "" msgid "Release|You can edit the content later by editing the release. %{linkStart}How do I edit a release?%{linkEnd}" @@ -34256,6 +34250,9 @@ msgstr "" msgid "Selected projects" msgstr "" +msgid "Selected tag is already in use. Choose another option." +msgstr "" + msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By %{link_open}@johnsmith%{link_close}\"). It will also associate and/or assign these issues and comments with the selected user." msgstr "" @@ -34553,6 +34550,9 @@ msgstr "" msgid "Set time estimate to %{time_estimate}." msgstr "" +msgid "Set to 0 for no size limit." +msgstr "" + msgid "Set up CI/CD" msgstr "" @@ -36815,7 +36815,7 @@ msgstr "" msgid "Tag name" msgstr "" -msgid "Tag name is required" +msgid "Tag name is required." msgstr "" msgid "Tag push" @@ -37746,6 +37746,9 @@ msgstr "" msgid "The project is still being deleted. Please try again later." msgstr "" +msgid "The project size exceeds the export limit." +msgstr "" + msgid "The project was successfully forked." msgstr "" diff --git a/qa/qa/support/knapsack_report.rb b/qa/qa/support/knapsack_report.rb index b8cf6743746..998802fe8b7 100644 --- a/qa/qa/support/knapsack_report.rb +++ b/qa/qa/support/knapsack_report.rb @@ -32,6 +32,8 @@ module QA # @return [void] def download_report logger.debug("Downloading latest knapsack report for '#{report_name}' to '#{report_path}'") + return logger.debug("Report already exists, skipping!") if File.exist?(report_path) + file = client.get_object(BUCKET, report_file) File.write(report_path, file[:body]) rescue StandardError => e @@ -146,7 +148,7 @@ module QA # # @return [String] def report_name - @report_name ||= ENV["CI_JOB_NAME"].split(" ").first.tr(":", "-") + @report_name ||= ENV["QA_KNAPSACK_REPORT_NAME"] || ENV["CI_JOB_NAME"].split(" ").first.tr(":", "-") end # GCS credentials json diff --git a/qa/tasks/knapsack.rake b/qa/tasks/knapsack.rake index 61c153291b7..cfc11d0ba24 100644 --- a/qa/tasks/knapsack.rake +++ b/qa/tasks/knapsack.rake @@ -18,7 +18,7 @@ namespace :knapsack do desc "Download latest knapsack report" task :download do - QA::Support::KnapsackReport.download + QA::Support::KnapsackReport.download_report end desc "Merge and upload knapsack report" diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index a69317032ef..537f7aa5fee 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1454,6 +1454,41 @@ RSpec.describe ProjectsController do expect(response).to have_gitlab_http_status(:found) end + + context 'when the project storage_size exceeds the application setting max_export_size' do + it 'returns 302 with alert' do + stub_application_setting(max_export_size: 1) + project.statistics.update!(lfs_objects_size: 2.megabytes, repository_size: 2.megabytes) + + post action, params: { namespace_id: project.namespace, id: project } + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:alert]).to include('The project size exceeds the export limit.') + end + end + + context 'when the project storage_size does not exceed the application setting max_export_size' do + it 'returns 302 without alert' do + stub_application_setting(max_export_size: 1) + project.statistics.update!(lfs_objects_size: 0.megabytes, repository_size: 0.megabytes) + + post action, params: { namespace_id: project.namespace, id: project } + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:alert]).to be_nil + end + end + + context 'when application setting max_export_size is not set' do + it 'returns 302 without alert' do + project.statistics.update!(lfs_objects_size: 2.megabytes, repository_size: 2.megabytes) + + post action, params: { namespace_id: project.namespace, id: project } + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:alert]).to be_nil + end + end end context 'when project export is disabled' do diff --git a/spec/factories/clusters/agent_tokens.rb b/spec/factories/clusters/agent_tokens.rb index 03f765123db..3ca6c95d0df 100644 --- a/spec/factories/clusters/agent_tokens.rb +++ b/spec/factories/clusters/agent_tokens.rb @@ -3,6 +3,7 @@ FactoryBot.define do factory :cluster_agent_token, class: 'Clusters::AgentToken' do association :agent, factory: :cluster_agent + association :created_by_user, factory: :user token_encrypted { Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(50)) } diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index 6b800e3d790..a7478ce2657 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -5,6 +5,16 @@ FactoryBot.define do title key { SSHData::PrivateKey::RSA.generate(1024, unsafe_allow_small_key: true).public_key.openssh(comment: 'dummy@gitlab.com') } + trait :expired do + to_create { |key| key.save!(validate: false) } + expires_at { 2.days.ago } + end + + trait :expired_today do + to_create { |key| key.save!(validate: false) } + expires_at { Date.today.beginning_of_day + 3.hours } + end + factory :key_without_comment do key { SSHData::PrivateKey::RSA.generate(1024, unsafe_allow_small_key: true).public_key.openssh } end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 32ae9c00c9e..daccd3a3925 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -111,6 +111,16 @@ RSpec.describe 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" end + it 'change Maximum export size' do + page.within('.as-account-limit') do + fill_in 'Maximum export size (MB)', with: 25 + click_button 'Save changes' + end + + expect(current_settings.max_export_size).to eq 25 + expect(page).to have_content "Application settings saved successfully" + end + it 'change Maximum import size' do page.within('.as-account-limit') do fill_in 'Maximum import size (MB)', with: 15 diff --git a/spec/fixtures/api/schemas/public_api/v4/agent_token.json b/spec/fixtures/api/schemas/public_api/v4/agent_token.json new file mode 100644 index 00000000000..8251ddd4de1 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/agent_token.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "description", + "agent_id", + "status", + "created_at", + "created_by_user_id", + "last_used_at" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "agent_id": { "type": "integer" }, + "status": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "created_by_user_id": { "type": "integer" }, + "last_used_at": { "type": ["string", "null"], "format": "date-time" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/agent_token_basic.json b/spec/fixtures/api/schemas/public_api/v4/agent_token_basic.json new file mode 100644 index 00000000000..90d9f35d0e1 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/agent_token_basic.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "description", + "agent_id", + "status", + "created_at", + "created_by_user_id" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "agent_id": { "type": "integer" }, + "status": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "created_by_user_id": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/agent_token_with_token.json b/spec/fixtures/api/schemas/public_api/v4/agent_token_with_token.json new file mode 100644 index 00000000000..99d80817d0f --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/agent_token_with_token.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "description", + "agent_id", + "status", + "created_at", + "created_by_user_id", + "last_used_at", + "token" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "agent_id": { "type": "integer" }, + "status": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "created_by_user_id": { "type": "integer" }, + "last_used_at": { "type": ["string", "null"], "format": "date-time" }, + "token": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/agent_tokens.json b/spec/fixtures/api/schemas/public_api/v4/agent_tokens.json new file mode 100644 index 00000000000..f3d14d09b3d --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/agent_tokens.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "agent_token_basic.json" } +} diff --git a/spec/fixtures/markdown/markdown_golden_master_examples.yml b/spec/fixtures/markdown/markdown_golden_master_examples.yml index 08b456d276b..5847e9f2cdf 100644 --- a/spec/fixtures/markdown/markdown_golden_master_examples.yml +++ b/spec/fixtures/markdown/markdown_golden_master_examples.yml @@ -750,7 +750,7 @@ markdown: |- Hi @gfm_user - thank you for reporting this bug (#1) we hope to fix it in %1.1 as part of !1 html: |- - <p data-sourcepos="1:1-1:92" dir="auto">Hi <a href="/gfm_user" data-user="1" data-reference-type="user" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="John Doe1">@gfm_user</a> - thank you for reporting this bug (<a href="/group1/project1/-/issues/1" data-original="#1" data-link="false" data-link-reference="false" data-project="11" data-issue="11" data-reference-type="issue" data-container="body" data-placement="top" title="My title 1" class="gfm gfm-issue has-tooltip">#1</a>) we hope to fix it in <a href="/group1/project1/-/milestones/1" data-original="%1.1" data-link="false" data-link-reference="false" data-project="11" data-milestone="11" data-reference-type="milestone" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%1.1</a> as part of <a href="/group1/project1/-/merge_requests/1" data-original="!1" data-link="false" data-link-reference="false" data-project="11" data-merge-request="11" data-project-path="group1/project1" data-iid="1" data-mr-title="My title 2" data-reference-type="merge_request" data-container="body" data-placement="top" title="" class="gfm gfm-merge_request">!1</a></p> + <p data-sourcepos="1:1-1:92" dir="auto">Hi <a href="/gfm_user" data-user="1" data-reference-type="user" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="John Doe1">@gfm_user</a> - thank you for reporting this bug (<a href="/group1/project1/-/issues/1" data-original="#1" data-link="false" data-link-reference="false" data-project="11" data-issue="11" data-issue-type="issue" data-reference-type="issue" data-container="body" data-placement="top" title="My title 1" class="gfm gfm-issue has-tooltip">#1</a>) we hope to fix it in <a href="/group1/project1/-/milestones/1" data-original="%1.1" data-link="false" data-link-reference="false" data-project="11" data-milestone="11" data-reference-type="milestone" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%1.1</a> as part of <a href="/group1/project1/-/merge_requests/1" data-original="!1" data-link="false" data-link-reference="false" data-project="11" data-merge-request="11" data-project-path="group1/project1" data-iid="1" data-mr-title="My title 2" data-reference-type="merge_request" data-container="body" data-placement="top" title="" class="gfm gfm-merge_request">!1</a></p> - name: strike markdown: |- ~~del~~ diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js index f2027252f05..37b897bf65d 100644 --- a/spec/frontend/environments/environment_folder_spec.js +++ b/spec/frontend/environments/environment_folder_spec.js @@ -15,12 +15,13 @@ Vue.use(VueApollo); describe('~/environments/components/environments_folder.vue', () => { let wrapper; let environmentFolderMock; + let intervalMock; let nestedEnvironment; const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') }); const createApolloProvider = () => { - const mockResolvers = { Query: { folder: environmentFolderMock } }; + const mockResolvers = { Query: { folder: environmentFolderMock, interval: intervalMock } }; return createMockApollo([], mockResolvers); }; @@ -40,6 +41,8 @@ describe('~/environments/components/environments_folder.vue', () => { environmentFolderMock = jest.fn(); [nestedEnvironment] = resolvedEnvironmentsApp.environments; environmentFolderMock.mockReturnValue(resolvedFolder); + intervalMock = jest.fn(); + intervalMock.mockReturnValue(2000); }); afterEach(() => { @@ -70,6 +73,8 @@ describe('~/environments/components/environments_folder.vue', () => { beforeEach(() => { collapse = wrapper.findComponent(GlCollapse); icons = wrapper.findAllComponents(GlIcon); + jest.spyOn(wrapper.vm.$apollo.queries.folder, 'startPolling'); + jest.spyOn(wrapper.vm.$apollo.queries.folder, 'stopPolling'); }); it('is collapsed by default', () => { @@ -93,6 +98,8 @@ describe('~/environments/components/environments_folder.vue', () => { expect(iconNames).toEqual(['angle-down', 'folder-open']); expect(folderName.classes('gl-font-weight-bold')).toBe(true); expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath); + + expect(wrapper.vm.$apollo.queries.folder.startPolling).toHaveBeenCalledWith(2000); }); it('displays all environments when opened', async () => { @@ -106,6 +113,16 @@ describe('~/environments/components/environments_folder.vue', () => { .wrappers.map((w) => w.text()); expect(environments).toEqual(expect.arrayContaining(names)); }); + + it('stops polling on click', async () => { + await button.trigger('click'); + expect(wrapper.vm.$apollo.queries.folder.startPolling).toHaveBeenCalledWith(2000); + + const collapseButton = wrapper.findByRole('button', { name: __('Collapse') }); + await collapseButton.trigger('click'); + + expect(wrapper.vm.$apollo.queries.folder.stopPolling).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb index 25049ee4722..e17e73a93c4 100644 --- a/spec/frontend/fixtures/runner.rb +++ b/spec/frontend/fixtures/runner.rb @@ -67,7 +67,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end describe GraphQL::Query, type: :request do - runner_query = 'details/runner.query.graphql' + runner_query = 'show/runner.query.graphql' let_it_be(:query) do get_graphql_query_as_string("#{query_path}#{runner_query}") @@ -91,7 +91,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end describe GraphQL::Query, type: :request do - runner_projects_query = 'details/runner_projects.query.graphql' + runner_projects_query = 'show/runner_projects.query.graphql' let_it_be(:query) do get_graphql_query_as_string("#{query_path}#{runner_projects_query}") @@ -107,7 +107,23 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end describe GraphQL::Query, type: :request do - runner_jobs_query = 'details/runner_jobs.query.graphql' + runner_jobs_query = 'show/runner_jobs.query.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{runner_jobs_query}") + end + + it "#{fixtures_path}#{runner_jobs_query}.json" do + post_graphql(query, current_user: admin, variables: { + id: instance_runner.to_global_id.to_s + }) + + expect_graphql_errors_to_be_empty + end + end + + describe GraphQL::Query, type: :request do + runner_jobs_query = 'edit/runner_form.query.graphql' let_it_be(:query) do get_graphql_query_as_string("#{query_path}#{runner_jobs_query}") diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js index ede2c384233..9f500c318ea 100644 --- a/spec/frontend/releases/components/tag_field_new_spec.js +++ b/spec/frontend/releases/components/tag_field_new_spec.js @@ -188,6 +188,18 @@ describe('releases/components/tag_field_new', () => { await expectValidationMessageToBe('hidden'); }); + + it('displays a validation error if the tag has an associated release', async () => { + findTagNameDropdown().vm.$emit('input', 'vTest'); + findTagNameDropdown().vm.$emit('hide'); + + store.state.editNew.existingRelease = {}; + + await expectValidationMessageToBe('shown'); + expect(findTagNameFormGroup().text()).toContain( + __('Selected tag is already in use. Choose another option.'), + ); + }); }); describe('when the user has interacted with the component and the value is empty', () => { @@ -196,6 +208,7 @@ describe('releases/components/tag_field_new', () => { findTagNameDropdown().vm.$emit('hide'); await expectValidationMessageToBe('shown'); + expect(findTagNameFormGroup().text()).toContain(__('Tag name is required.')); }); }); }); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index c2e6c2851a8..41653f62ebf 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -608,7 +608,7 @@ describe('Release edit/new actions', () => { ); expect(createFlash).toHaveBeenCalledWith({ - message: s__('Release|Something went wrong while getting the tag notes.'), + message: s__('Release|Unable to fetch the tag notes.'), }); expect(getTag).toHaveBeenCalledWith(state.projectId, tagName); }); diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js index 3a3289c9cf0..c42c6c00f56 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -146,6 +146,8 @@ describe('Release edit/new getters', () => { ], }, }, + // tag has an existing release + existingRelease: {}, }; actualErrors = getters.validationErrors(state); @@ -159,6 +161,14 @@ describe('Release edit/new getters', () => { expect(actualErrors).toMatchObject(expectedErrors); }); + it('returns a validation error if the tag has an existing release', () => { + const expectedErrors = { + existingRelease: true, + }; + + expect(actualErrors).toMatchObject(expectedErrors); + }); + it('returns a validation error if links share a URL', () => { const expectedErrors = { assets: { diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index 0c97524b704..85844831e0b 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -249,9 +249,10 @@ describe('Release edit/new mutations', () => { state.isFetchingTagNotes = true; const message = 'tag notes'; - mutations[types.RECEIVE_TAG_NOTES_SUCCESS](state, { message }); + mutations[types.RECEIVE_TAG_NOTES_SUCCESS](state, { message, release }); expect(state.tagNotes).toBe(message); expect(state.isFetchingTagNotes).toBe(false); + expect(state.existingRelease).toBe(release); }); }); describe(`${types.RECEIVE_TAG_NOTES_ERROR}`, () => { diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js index f40eb6938a8..8a34cb14d8b 100644 --- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js +++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js @@ -8,16 +8,16 @@ import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/runner/components/runner_header.vue'; import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; -import runnerQuery from '~/runner/graphql/details/runner.query.graphql'; +import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql'; import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue'; import { captureException } from '~/runner/sentry_utils'; -import { runnerData } from '../mock_data'; +import { runnerFormData } from '../mock_data'; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); -const mockRunner = runnerData.data.runner; +const mockRunner = runnerFormData.data.runner; const mockRunnerGraphqlId = mockRunner.id; const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; const mockRunnerPath = `/admin/runners/${mockRunnerId}`; @@ -33,7 +33,7 @@ describe('AdminRunnerEditApp', () => { const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { wrapper = mountFn(AdminRunnerEditApp, { - apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]), + apolloProvider: createMockApollo([[runnerFormQuery, mockRunnerQuery]]), propsData: { runnerId: mockRunnerId, runnerPath: mockRunnerPath, @@ -45,7 +45,7 @@ describe('AdminRunnerEditApp', () => { }; beforeEach(() => { - mockRunnerQuery = jest.fn().mockResolvedValue(runnerData); + mockRunnerQuery = jest.fn().mockResolvedValue(runnerFormData); }); afterEach(() => { diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js index 9ac1e772418..07259ec3538 100644 --- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -11,7 +11,7 @@ import RunnerHeader from '~/runner/components/runner_header.vue'; import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; -import runnerQuery from '~/runner/graphql/details/runner.query.graphql'; +import runnerQuery from '~/runner/graphql/show/runner.query.graphql'; import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue'; import { captureException } from '~/runner/sentry_utils'; import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage'; diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js index 9e40e911448..8ac5685a0dd 100644 --- a/spec/frontend/runner/components/runner_jobs_spec.js +++ b/spec/frontend/runner/components/runner_jobs_spec.js @@ -11,7 +11,7 @@ import RunnerPagination from '~/runner/components/runner_pagination.vue'; import { captureException } from '~/runner/sentry_utils'; import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants'; -import runnerJobsQuery from '~/runner/graphql/details/runner_jobs.query.graphql'; +import runnerJobsQuery from '~/runner/graphql/show/runner_jobs.query.graphql'; import { runnerData, runnerJobsData } from '../mock_data'; diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js index 62ebc6539e2..04627e2307b 100644 --- a/spec/frontend/runner/components/runner_projects_spec.js +++ b/spec/frontend/runner/components/runner_projects_spec.js @@ -16,7 +16,7 @@ import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; import { captureException } from '~/runner/sentry_utils'; -import runnerProjectsQuery from '~/runner/graphql/details/runner_projects.query.graphql'; +import runnerProjectsQuery from '~/runner/graphql/show/runner_projects.query.graphql'; import { runnerData, runnerProjectsData } from '../mock_data'; diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index dbe15f8e9c6..3037364d941 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -14,17 +14,17 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED, } from '~/runner/constants'; -import runnerUpdateMutation from '~/runner/graphql/details/runner_update.mutation.graphql'; +import runnerUpdateMutation from '~/runner/graphql/edit/runner_update.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage'; -import { runnerData } from '../mock_data'; +import { runnerFormData } from '../mock_data'; jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage'); jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility'); -const mockRunner = runnerData.data.runner; +const mockRunner = runnerFormData.data.runner; const mockRunnerPath = '/admin/runners/1'; Vue.use(VueApollo); @@ -127,24 +127,7 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); // Some read-only fields are not submitted - const { - __typename, - shortSha, - ipAddress, - executorName, - architectureName, - platformName, - runnerType, - createdAt, - status, - editAdminUrl, - contactedAt, - userPermissions, - version, - groups, - jobCount, - ...submitted - } = mockRunner; + const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner; expectToHaveSubmittedRunnerContaining(submitted); }); diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index fbe8926124c..e4351e9c90c 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -8,11 +8,14 @@ import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.qu import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json'; import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json'; -// Details queries -import runnerData from 'test_fixtures/graphql/runner/details/runner.query.graphql.json'; -import runnerWithGroupData from 'test_fixtures/graphql/runner/details/runner.query.graphql.with_group.json'; -import runnerProjectsData from 'test_fixtures/graphql/runner/details/runner_projects.query.graphql.json'; -import runnerJobsData from 'test_fixtures/graphql/runner/details/runner_jobs.query.graphql.json'; +// Show runner queries +import runnerData from 'test_fixtures/graphql/runner/show/runner.query.graphql.json'; +import runnerWithGroupData from 'test_fixtures/graphql/runner/show/runner.query.graphql.with_group.json'; +import runnerProjectsData from 'test_fixtures/graphql/runner/show/runner_projects.query.graphql.json'; +import runnerJobsData from 'test_fixtures/graphql/runner/show/runner_jobs.query.graphql.json'; + +// Edit runner queries +import runnerFormData from 'test_fixtures/graphql/runner/edit/runner_form.query.graphql.json'; // Other mock data export const onlineContactTimeoutSecs = 2 * 60 * 60; @@ -20,13 +23,14 @@ export const staleTimeoutSecs = 5259492; // Ruby's `2.months` export { runnersData, - runnersCountData, runnersDataPaginated, + runnersCountData, + groupRunnersData, + groupRunnersDataPaginated, + groupRunnersCountData, runnerData, runnerWithGroupData, runnerProjectsData, runnerJobsData, - groupRunnersData, - groupRunnersCountData, - groupRunnersDataPaginated, + runnerFormData, }; diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index c3a3c2a0178..399726263db 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -111,7 +111,6 @@ RSpec.describe ProfilesHelper do where(:error, :expired, :result) do false | false | nil true | false | error_message - false | true | 'Key usable beyond expiration date.' true | true | error_message end @@ -130,13 +129,9 @@ RSpec.describe ProfilesHelper do end describe "#ssh_key_expires_field_description" do - before do - allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false) - end + subject { helper.ssh_key_expires_field_description } - it 'returns the description' do - expect(helper.ssh_key_expires_field_description).to eq('Key can still be used after expiration.') - end + it { is_expected.to eq('Key becomes invalid on this date.') } end describe '#middle_dot_divider_classes' do diff --git a/spec/lib/api/entities/ci/job_request/dependency_spec.rb b/spec/lib/api/entities/ci/job_request/dependency_spec.rb index fa5f3da554c..b45885e9982 100644 --- a/spec/lib/api/entities/ci/job_request/dependency_spec.rb +++ b/spec/lib/api/entities/ci/job_request/dependency_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' RSpec.describe API::Entities::Ci::JobRequest::Dependency do + let(:running_job) { create(:ci_build, :artifacts) } let(:job) { create(:ci_build, :artifacts) } - let(:entity) { described_class.new(job) } + let(:entity) { described_class.new(job, { running_job: running_job }) } subject { entity.as_json } @@ -16,8 +17,18 @@ RSpec.describe API::Entities::Ci::JobRequest::Dependency do expect(subject[:name]).to eq(job.name) end - it 'returns the dependency token' do - expect(subject[:token]).to eq(job.token) + it 'returns the token belonging to the running job' do + expect(subject[:token]).to eq(running_job.token) + end + + context 'when ci_expose_running_job_token_for_artifacts is disabled' do + before do + stub_feature_flags(ci_expose_running_job_token_for_artifacts: false) + end + + it 'returns the token belonging to the dependency job' do + expect(subject[:token]).to eq(job.token) + end end it 'returns the dependency artifacts_file', :aggregate_failures do diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb index c493cb77c98..c6f0e592cdf 100644 --- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb @@ -15,6 +15,14 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do let(:issue_path) { "/#{issue.project.namespace.path}/#{issue.project.path}/-/issues/#{issue.iid}" } let(:issue_url) { "http://#{Gitlab.config.gitlab.host}#{issue_path}" } + shared_examples 'a reference with issue type information' do + it 'contains issue-type as a data attribute' do + doc = reference_filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('data-issue-type')).to eq('issue') + end + end + it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end @@ -44,6 +52,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") @@ -158,6 +168,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) .with(project2, issue.iid) @@ -208,6 +220,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) .with(project2, issue.iid) @@ -258,6 +272,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) .with(project2, issue.iid) @@ -307,6 +323,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -342,6 +360,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") @@ -371,6 +391,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index d6ef1836ad9..e628a06a542 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -228,6 +228,15 @@ RSpec.describe Gitlab::GitAccess do project.add_maintainer(user) end + context 'key is expired' do + let(:actor) { create(:rsa_key_2048, :expired) } + + it 'does not allow expired keys', :aggregate_failures do + expect { pull_access_check }.to raise_forbidden('Your SSH key has expired.') + expect { push_access_check }.to raise_forbidden('Your SSH key has expired.') + end + end + context 'key is too small' do before do stub_application_setting(rsa_key_restriction: 4096) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 541fa1ac77a..9f0f056d14e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -417,6 +417,14 @@ RSpec.describe ApplicationSetting do .is_greater_than(0) end + it { is_expected.to validate_presence_of(:max_export_size) } + + specify do + is_expected.to validate_numericality_of(:max_export_size) + .only_integer + .is_greater_than_or_equal_to(0) + end + it { is_expected.to validate_presence_of(:max_import_size) } specify do diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb index 3af717798c3..4ff743991a6 100644 --- a/spec/models/instance_configuration_spec.rb +++ b/spec/models/instance_configuration_spec.rb @@ -99,6 +99,7 @@ RSpec.describe InstanceConfiguration do max_attachment_size: 10, receive_max_input_size: 20, max_import_size: 30, + max_export_size: 40, diff_max_patch_bytes: 409600, max_artifacts_size: 50, max_pages_size: 60, @@ -112,6 +113,7 @@ RSpec.describe InstanceConfiguration do expect(size_limits[:max_attachment_size]).to eq(10.megabytes) expect(size_limits[:receive_max_input_size]).to eq(20.megabytes) expect(size_limits[:max_import_size]).to eq(30.megabytes) + expect(size_limits[:max_export_size]).to eq(40.megabytes) expect(size_limits[:diff_max_patch_bytes]).to eq(400.kilobytes) expect(size_limits[:max_artifacts_size]).to eq(50.megabytes) expect(size_limits[:max_pages_size]).to eq(60.megabytes) @@ -127,11 +129,16 @@ RSpec.describe InstanceConfiguration do end it 'returns nil if set to 0 (unlimited)' do - Gitlab::CurrentSettings.current_application_settings.update!(max_import_size: 0, max_pages_size: 0) + Gitlab::CurrentSettings.current_application_settings.update!( + max_import_size: 0, + max_export_size: 0, + max_pages_size: 0 + ) size_limits = subject.settings[:size_limits] expect(size_limits[:max_import_size]).to be_nil + expect(size_limits[:max_export_size]).to be_nil expect(size_limits[:max_pages_size]).to be_nil end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 6d01b6ae256..225c9714187 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -102,15 +102,15 @@ RSpec.describe Key, :mailer do context 'expiration scopes' do let_it_be(:user) { create(:user) } - let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user) } - let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user, expiry_notification_delivered_at: Time.current) } - let_it_be(:expired_yesterday) { create(:key, expires_at: 1.day.ago, user: user) } + let_it_be(:expired_today_not_notified) { create(:key, :expired_today, user: user) } + let_it_be(:expired_today_already_notified) { create(:key, :expired_today, user: user, expiry_notification_delivered_at: Time.current) } + let_it_be(:expired_yesterday) { create(:key, :expired, user: user) } let_it_be(:expiring_soon_unotified) { create(:key, expires_at: 3.days.from_now, user: user) } let_it_be(:expiring_soon_notified) { create(:key, expires_at: 4.days.from_now, user: user, before_expiry_notification_delivered_at: Time.current) } let_it_be(:future_expiry) { create(:key, expires_at: 1.month.from_now, user: user) } describe '.expired_today_and_not_notified' do - it 'returns keys that expire today and in the past' do + it 'returns keys that expire today and have not been notified' do expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d315b62b23e..1431a159f1d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7158,11 +7158,33 @@ RSpec.describe Project, factory_default: :keep do end describe '#add_export_job' do - context 'if not already present' do - it 'starts project export job' do - user = create(:user) - project = build(:project) + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + context 'when project storage_size does not exceed the application setting max_export_size' do + it 'starts project export worker' do + stub_application_setting(max_export_size: 1) + allow(project.statistics).to receive(:storage_size).and_return(0.megabytes) + + expect(ProjectExportWorker).to receive(:perform_async).with(user.id, project.id, nil, {}) + + project.add_export_job(current_user: user) + end + end + + context 'when project storage_size exceeds the application setting max_export_size' do + it 'raises Project::ExportLimitExceeded' do + stub_application_setting(max_export_size: 1) + allow(project.statistics).to receive(:storage_size).and_return(2.megabytes) + + expect(ProjectExportWorker).not_to receive(:perform_async).with(user.id, project.id, nil, {}) + expect { project.add_export_job(current_user: user) }.to raise_error(Project::ExportLimitExceeded) + end + end + context 'when application setting max_export_size is not set' do + it 'starts project export worker' do + allow(project.statistics).to receive(:storage_size).and_return(2.megabytes) expect(ProjectExportWorker).to receive(:perform_async).with(user.id, project.id, nil, {}) project.add_export_job(current_user: user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 05dcabd96ba..71171f98492 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1048,8 +1048,8 @@ RSpec.describe User do context 'SSH key expiration scopes' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } - let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user1) } - let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user2, expiry_notification_delivered_at: Time.current) } + let_it_be(:expired_today_not_notified) { create(:key, :expired_today, user: user1) } + let_it_be(:expired_today_already_notified) { create(:key, :expired_today, user: user2, expiry_notification_delivered_at: Time.current) } let_it_be(:expiring_soon_not_notified) { create(:key, expires_at: 2.days.from_now, user: user2) } let_it_be(:expiring_soon_notified) { create(:key, expires_at: 2.days.from_now, user: user1, before_expiry_notification_delivered_at: Time.current) } diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb index f627f207d98..dd9e2c32925 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -7,8 +7,20 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do include RedisHelpers include WorkhorseHelpers + let_it_be_with_reload(:parent_group) { create(:group) } + let_it_be_with_reload(:group) { create(:group, parent: parent_group) } + let_it_be_with_reload(:project) { create(:project, namespace: group, shared_runners_enabled: false) } + + let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } + let_it_be(:user) { create(:user) } + let(:registration_token) { 'abcdefg123456' } + before_all do + project.add_developer(user) + end + before do stub_feature_flags(ci_enable_live_trace: true) stub_gitlab_calls @@ -17,12 +29,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end describe '/api/v4/jobs' do - let(:parent_group) { create(:group) } - let(:group) { create(:group, parent: parent_group) } - let(:project) { create(:project, namespace: group, shared_runners_enabled: false) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } - let(:runner) { create(:ci_runner, :project, projects: [project]) } - let(:user) { create(:user) } let(:job) do create(:ci_build, :artifacts, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) @@ -817,25 +823,23 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when job has artifacts' do - let(:job) { create(:ci_build) } + let(:job) { create(:ci_build, pipeline: pipeline, user: user) } let(:store) { JobArtifactUploader::Store::LOCAL } before do create(:ci_job_artifact, :archive, file_store: store, job: job) end - context 'when using job token' do + shared_examples 'successful artifact download' do context 'when artifacts are stored locally' do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', 'Content-Disposition' => %q(attachment; filename="ci_build_artifacts.zip"; filename*=UTF-8''ci_build_artifacts.zip) } end - before do + it 'downloads artifacts' do download_artifact - end - it 'download artifacts' do expect(response).to have_gitlab_http_status(:ok) expect(response.headers.to_h).to include download_headers end @@ -843,26 +847,20 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when artifacts are stored remotely' do let(:store) { JobArtifactUploader::Store::REMOTE } - let!(:job) { create(:ci_build) } context 'when proxy download is being used' do - before do + it 'uses workhorse send-url' do download_artifact(direct_download: false) - end - it 'uses workhorse send-url' do expect(response).to have_gitlab_http_status(:ok) - expect(response.headers.to_h).to include( - 'Gitlab-Workhorse-Send-Data' => /send-url:/) + expect(response.headers.to_h).to include('Gitlab-Workhorse-Send-Data' => /send-url:/) end end context 'when direct download is being used' do - before do + it 'receives redirect for downloading artifacts' do download_artifact(direct_download: true) - end - it 'receive redirect for downloading artifacts' do expect(response).to have_gitlab_http_status(:found) expect(response.headers).to include('Location') end @@ -870,16 +868,151 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end end - context 'when using runnners token' do - let(:token) { job.project.runners_token } + shared_examples 'forbidden request' do + it 'responds with forbidden' do + download_artifact + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when using job token' do + let(:token) { job.token } + + it_behaves_like 'successful artifact download' + + context 'when the job is no longer running' do + before do + job.success! + end + + it_behaves_like 'successful artifact download' + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ci_authenticate_running_job_token_for_artifacts: false) + end + + it_behaves_like 'successful artifact download' + + context 'when the job is no longer running' do + before do + job.success! + end + + it_behaves_like 'successful artifact download' + end + end + end + + context 'when using token belonging to the dependent job' do + let!(:dependent_job) { create(:ci_build, :running, :dependent, user: user, pipeline: pipeline) } + let!(:job) { dependent_job.all_dependencies.first } + + let(:token) { dependent_job.token } + + it_behaves_like 'successful artifact download' + + context 'when the dependent job is no longer running' do + before do + dependent_job.success! + end + + it_behaves_like 'forbidden request' + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ci_authenticate_running_job_token_for_artifacts: false) + end + + it_behaves_like 'forbidden request' + end + end + + context 'when using token belonging to another job created by another project member' do + let!(:ci_build) { create(:ci_build, :running, :dependent, user: user, pipeline: pipeline) } + let!(:job) { ci_build.all_dependencies.first } + + let!(:another_dev) { create(:user) } + + let(:token) { ci_build.token } before do - download_artifact + project.add_developer(another_dev) + ci_build.update!(user: another_dev) end - it 'responds with forbidden' do - expect(response).to have_gitlab_http_status(:forbidden) + it_behaves_like 'successful artifact download' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ci_authenticate_running_job_token_for_artifacts: false) + end + + it_behaves_like 'forbidden request' + end + end + + context 'when using token belonging to a pending dependent job' do + let!(:ci_build) { create(:ci_build, :pending, :dependent, user: user, project: project, pipeline: pipeline) } + let!(:job) { ci_build.all_dependencies.first } + + let(:token) { ci_build.token } + + it_behaves_like 'forbidden request' + end + + context 'when using a token from a cross pipeline build' do + let!(:ci_build) { create(:ci_build, :pending, :dependent, user: user, project: project, pipeline: pipeline) } + let!(:job) { ci_build.all_dependencies.first } + + let!(:options) do + { + cross_dependencies: [ + { + pipeline: pipeline.id, + job: job.name, + artifacts: true + } + ] + + } end + + let!(:cross_pipeline) { create(:ci_pipeline, project: project, child_of: pipeline) } + let!(:cross_pipeline_build) { create(:ci_build, :running, project: project, user: user, options: options, pipeline: cross_pipeline) } + + let(:token) { cross_pipeline_build.token } + + before do + job.success! + end + + it_behaves_like 'successful artifact download' + end + + context 'when using a token from an unrelated project' do + let!(:ci_build) { create(:ci_build, :running, :dependent, user: user, project: project, pipeline: pipeline) } + let!(:job) { ci_build.all_dependencies.first } + + let!(:unrelated_ci_build) { create(:ci_build, :running, user: create(:user)) } + let(:token) { unrelated_ci_build.token } + + it_behaves_like 'forbidden request' + end + + context 'when using runnners token' do + let(:token) { job.project.runners_token } + + it_behaves_like 'forbidden request' + end + + context 'when using an invalid token' do + let(:token) { 'invalid-token' } + + it_behaves_like 'forbidden request' end end diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index a662c77e5a2..b54ad811e6e 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -496,15 +496,32 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do job2.success end - it 'returns dependent jobs' do + it 'returns dependent jobs with the token of the test job' do request_job expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(2) expect(json_response['dependencies']).to include( - { 'id' => job.id, 'name' => job.name, 'token' => job.token }, - { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + { 'id' => job.id, 'name' => job.name, 'token' => test_job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => test_job.token }) + end + + context 'when ci_expose_running_job_token_for_artifacts is disabled' do + before do + stub_feature_flags(ci_expose_running_job_token_for_artifacts: false) + end + + it 'returns dependent jobs with the dependency job tokens' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(2) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end end describe 'preloading job_artifacts_archive' do @@ -526,14 +543,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do job.success end - it 'returns dependent jobs' do + it 'returns dependent jobs with the token of the test job' do request_job expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(1) expect(json_response['dependencies']).to include( - { 'id' => job.id, 'name' => job.name, 'token' => job.token, + { 'id' => job.id, 'name' => job.name, 'token' => test_job.token, 'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 107464 } }) end end @@ -552,13 +569,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do job2.success end - it 'returns dependent jobs' do + it 'returns dependent jobs with the token of the test job' do request_job expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(1) - expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token) + expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => test_job.token) end end diff --git a/spec/requests/api/clusters/agent_tokens_spec.rb b/spec/requests/api/clusters/agent_tokens_spec.rb new file mode 100644 index 00000000000..ba26faa45a3 --- /dev/null +++ b/spec/requests/api/clusters/agent_tokens_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Clusters::AgentTokens do + let_it_be(:agent) { create(:cluster_agent) } + let_it_be(:agent_token_one) { create(:cluster_agent_token, agent: agent) } + let_it_be(:agent_token_two) { create(:cluster_agent_token, agent: agent) } + let_it_be(:project) { agent.project } + let_it_be(:user) { agent.created_by_user } + let_it_be(:unauthorized_user) { create(:user) } + + before_all do + project.add_maintainer(user) + project.add_guest(unauthorized_user) + end + + describe 'GET /projects/:id/cluster_agents/:agent_id/tokens' do + context 'with authorized user' do + it 'returns tokens' do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/agent_tokens') + expect(json_response.count).to eq(2) + expect(json_response.first['name']).to eq(agent_token_one.name) + expect(json_response.first['agent_id']).to eq(agent.id) + expect(json_response.second['name']).to eq(agent_token_two.name) + expect(json_response.second['agent_id']).to eq(agent.id) + end + end + end + + context 'with unauthorized user' do + it 'cannot access agent tokens' do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + it 'avoids N+1 queries', :request_store do + # Establish baseline + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user) + + control = ActiveRecord::QueryRecorder.new do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user) + expect(response).to have_gitlab_http_status(:ok) + end + + # Now create a second record and ensure that the API does not execute + # any more queries than before + create(:cluster_agent_token, agent: agent) + + expect do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user) + end.not_to exceed_query_limit(control) + end + end + + describe 'GET /projects/:id/cluster_agents/:agent_id/tokens/:token_id' do + context 'with authorized user' do + it 'returns an agent token' do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{agent_token_one.id}", user) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/agent_token') + expect(json_response['id']).to eq(agent_token_one.id) + expect(json_response['name']).to eq(agent_token_one.name) + expect(json_response['agent_id']).to eq(agent.id) + end + end + + it 'returns a 404 error if agent token id is not available' do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with unauthorized user' do + it 'cannot access single agent token' do + get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{agent_token_one.id}", unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'cannot access token from agent of another project' do + another_project = create(:project, namespace: unauthorized_user.namespace) + another_agent = create(:cluster_agent, project: another_project, created_by_user: unauthorized_user) + + get api("/projects/#{another_project.id}/cluster_agents/#{another_agent.id}/tokens/#{agent_token_one.id}", + unauthorized_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'POST /projects/:id/cluster_agents/:agent_id/tokens' do + it 'creates a new agent token' do + params = { + name: 'test-token', + description: 'Test description' + } + post(api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user), params: params) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/agent_token_with_token') + expect(json_response['name']).to eq(params[:name]) + expect(json_response['description']).to eq(params[:description]) + expect(json_response['agent_id']).to eq(agent.id) + end + end + + it 'returns a 400 error if name not given' do + post api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 404 error if project does not exist' do + post api("/projects/#{non_existing_record_id}/cluster_agents/tokens", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 error if agent does not exist' do + post api("/projects/#{project.id}/cluster_agents/#{non_existing_record_id}/tokens", user), + params: { name: "some" } + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'with unauthorized user' do + it 'prevents to create agent token' do + post api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", unauthorized_user), + params: { name: "some" } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'DELETE /projects/:id/cluster_agents/:agent_id/tokens/:token_id' do + it 'revokes agent token' do + delete api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{agent_token_one.id}", user) + + expect(response).to have_gitlab_http_status(:no_content) + expect(agent_token_one.reload).to be_revoked + end + + it 'returns a 404 error when revoking non existent agent token' do + delete api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns a 404 if the user is unauthorized to revoke' do + delete api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{agent_token_one.id}", unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'cannot revoke token from agent of another project' do + another_project = create(:project, namespace: unauthorized_user.namespace) + another_agent = create(:cluster_agent, project: another_project, created_by_user: unauthorized_user) + + delete api("/projects/#{another_project.id}/cluster_agents/#{another_agent.id}/tokens/#{agent_token_one.id}", + unauthorized_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 2f452ce806e..c33e2bce65e 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -802,13 +802,13 @@ RSpec.describe API::Internal::Base do context 'git pull' do context 'with a key that has expired' do - let(:key) { create(:key, user: user, expires_at: 2.days.ago) } + let(:key) { create(:key, :expired, user: user) } - it 'includes the `key expired` message in the response' do + it 'includes the `key expired` message in the response and fails' do pull(key, project) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['gl_console_messages']).to eq(['INFO: Your SSH key has expired. Please generate a new key.']) + expect(response).to have_gitlab_http_status(:unauthorized) + expect(json_response['message']).to eq('Your SSH key has expired.') end end diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 07efd56fef4..8a8cd8512f8 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -410,6 +410,27 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do it_behaves_like 'post project export start' + context 'with project export size limit' do + before do + stub_application_setting(max_export_size: 1) + end + + it 'starts if limit not exceeded' do + post api(path, user) + + expect(response).to have_gitlab_http_status(:accepted) + end + + it '400 response if limit exceeded' do + project.statistics.update!(lfs_objects_size: 2.megabytes, repository_size: 2.megabytes) + + post api(path, user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response["message"]).to include('The project size exceeds the export limit.') + end + end + context 'when rate limit is exceeded across projects' do before do allow(Gitlab::ApplicationRateLimiter) diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index c724c69045e..2c1b4dc8da2 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -54,6 +54,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response['runner_token_expiration_interval']).to be_nil expect(json_response['group_runner_token_expiration_interval']).to be_nil expect(json_response['project_runner_token_expiration_interval']).to be_nil + expect(json_response['max_export_size']).to eq(0) end end @@ -138,6 +139,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do spam_check_api_key: 'SPAM_CHECK_API_KEY', mailgun_events_enabled: true, mailgun_signing_key: 'MAILGUN_SIGNING_KEY', + max_export_size: 6, disabled_oauth_sign_in_sources: 'unknown', import_sources: 'github,bitbucket', wiki_page_max_content_bytes: 12345, @@ -193,6 +195,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY') expect(json_response['mailgun_events_enabled']).to be(true) expect(json_response['mailgun_signing_key']).to eq('MAILGUN_SIGNING_KEY') + expect(json_response['max_export_size']).to eq(6) expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) expect(json_response['import_sources']).to match_array(%w(github bitbucket)) expect(json_response['wiki_page_max_content_bytes']).to eq(12345) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c554463df76..040ac4f74a7 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1679,13 +1679,13 @@ RSpec.describe API::Users do end it 'creates SSH key with `expires_at` attribute' do - optional_attributes = { expires_at: '2016-01-21T00:00:00.000Z' } + optional_attributes = { expires_at: 3.weeks.from_now } attributes = attributes_for(:key).merge(optional_attributes) post api("/users/#{user.id}/keys", admin), params: attributes expect(response).to have_gitlab_http_status(:created) - expect(json_response['expires_at']).to eq(optional_attributes[:expires_at]) + expect(json_response['expires_at'].to_date).to eq(optional_attributes[:expires_at].to_date) end it "returns 400 for invalid ID" do @@ -2373,13 +2373,13 @@ RSpec.describe API::Users do end it 'creates SSH key with `expires_at` attribute' do - optional_attributes = { expires_at: '2016-01-21T00:00:00.000Z' } + optional_attributes = { expires_at: 3.weeks.from_now } attributes = attributes_for(:key).merge(optional_attributes) post api("/user/keys", user), params: attributes expect(response).to have_gitlab_http_status(:created) - expect(json_response['expires_at']).to eq(optional_attributes[:expires_at]) + expect(json_response['expires_at'].to_date).to eq(optional_attributes[:expires_at].to_date) end it "returns a 401 error if unauthorized" do diff --git a/spec/services/keys/expiry_notification_service_spec.rb b/spec/services/keys/expiry_notification_service_spec.rb index 1d1da179cf7..7cb6cbce311 100644 --- a/spec/services/keys/expiry_notification_service_spec.rb +++ b/spec/services/keys/expiry_notification_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Keys::ExpiryNotificationService do end context 'with key expiring today', :mailer do - let_it_be_with_reload(:key) { create(:key, expires_at: Time.current, user: user) } + let_it_be_with_reload(:key) { create(:key, expires_at: 10.minutes.from_now, user: user) } let(:expiring_soon) { false } diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index 348ea9ad7d4..d40a87e5165 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -21,6 +21,14 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do let(:target_branch) { 'feature' } let(:title) { 'my title' } let(:description) { 'my description' } + let(:multiline_description) do + <<~MD.chomp + Line 1 + Line 2 + Line 3 + MD + end + let(:label1) { 'mylabel1' } let(:label2) { 'mylabel2' } let(:label3) { 'mylabel3' } @@ -64,6 +72,16 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do end end + shared_examples_for 'a service that can set the multiline description of a merge request' do + subject(:last_mr) { MergeRequest.last } + + it 'sets the multiline description' do + service.execute + + expect(last_mr.description).to eq(multiline_description) + end + end + shared_examples_for 'a service that can set the milestone of a merge request' do subject(:last_mr) { MergeRequest.last } @@ -417,6 +435,13 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the description of a merge request' + + context 'with a multiline description' do + let(:push_options) { { description: "Line 1\\nLine 2\\nLine 3" } } + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can set the multiline description of a merge request' + end end it_behaves_like 'with a deleted branch' diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb index ba8394178d9..c807512a11a 100644 --- a/spec/views/profiles/keys/_form.html.haml_spec.rb +++ b/spec/views/profiles/keys/_form.html.haml_spec.rb @@ -15,8 +15,6 @@ RSpec.describe 'profiles/keys/_form.html.haml' do context 'when the form partial is used' do before do - allow(view).to receive(:ssh_key_expires_field_description).and_return('Key can still be used after expiration.') - render end @@ -37,7 +35,7 @@ RSpec.describe 'profiles/keys/_form.html.haml' do it 'has the expires at field', :aggregate_failures do expect(rendered).to have_field('Expiration date', type: 'date') expect(page.find_field('Expiration date')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d")) - expect(rendered).to have_text('Key can still be used after expiration.') + expect(rendered).to have_text('Key becomes invalid on this date') end it 'has the validation warning', :aggregate_failures do diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb index ed8026d2453..1040541332d 100644 --- a/spec/views/profiles/keys/_key.html.haml_spec.rb +++ b/spec/views/profiles/keys/_key.html.haml_spec.rb @@ -59,11 +59,7 @@ RSpec.describe 'profiles/keys/_key.html.haml' do end context 'when the key has expired' do - let_it_be(:key) do - create(:personal_key, - user: user, - expires_at: 2.days.ago) - end + let_it_be(:key) { create(:personal_key, :expired, user: user) } it 'renders "Expired:" as the expiration date label' do render @@ -91,8 +87,6 @@ RSpec.describe 'profiles/keys/_key.html.haml' do where(:valid, :expiry, :result) do false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, ED25519, ECDSA_SK, or ED25519_SK' - false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, ED25519, ECDSA_SK, or ED25519_SK' - true | 2.days.ago | 'Key usable beyond expiration date.' true | 2.days.from_now | '' end diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb index be38391ff8c..26d9460d73e 100644 --- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb +++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb @@ -16,12 +16,12 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do let_it_be(:user) { create(:user) } context 'with a large batch' do + let_it_be_with_reload(:keys) { create_list(:key, 20, :expired_today, user: user) } + before do stub_const("SshKeys::ExpiredNotificationWorker::BATCH_SIZE", 5) end - let_it_be_with_reload(:keys) { create_list(:key, 20, expires_at: Time.current, user: user) } - it 'updates all keys regardless of batch size' do worker.perform @@ -30,7 +30,7 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do end context 'with expiring key today' do - let_it_be_with_reload(:expired_today) { create(:key, expires_at: Time.current, user: user) } + let_it_be_with_reload(:expired_today) { create(:key, :expired_today, user: user) } it 'invoke the notification service' do expect_next_instance_of(Keys::ExpiryNotificationService) do |expiry_service| @@ -52,7 +52,7 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do end context 'when key has expired in the past' do - let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) } + let_it_be(:expired_past) { create(:key, :expired, user: user) } it 'does not update notified column' do expect { worker.perform }.not_to change { expired_past.reload.expiry_notification_delivered_at } @@ -60,7 +60,7 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do context 'when key has already been notified of expiration' do before do - expired_past.update!(expiry_notification_delivered_at: 1.day.ago) + expired_past.update_attribute(:expiry_notification_delivered_at, 1.day.ago) end it 'does not update notified column' do diff --git a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb index 0a1d4a14ad0..e907d035020 100644 --- a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb +++ b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb @@ -38,7 +38,7 @@ RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do end context 'when key has expired in the past' do - let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) } + let_it_be(:expired_past) { create(:key, :expired, user: user) } it 'does not update notified column' do expect { worker.perform }.not_to change { expired_past.reload.before_expiry_notification_delivered_at } |