diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 09:08:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 09:08:29 +0300 |
commit | 654859099919ed5fd1896956460ba00568a2d90e (patch) | |
tree | 94b7ac45a50f75d674dc9a32d24639bee73bf8ed | |
parent | 5ea8a46ef44de37afd98447e8a38f36f925d0af8 (diff) |
Add latest changes from gitlab-org/gitlab@master
-rw-r--r-- | app/assets/javascripts/boards/components/board_form.vue | 10 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue | 56 | ||||
-rw-r--r-- | config/feature_flags/development/incremental_repository_backup.yml | 8 | ||||
-rw-r--r-- | doc/api/environments.md | 87 | ||||
-rw-r--r-- | doc/raketasks/backup_restore.md | 19 | ||||
-rw-r--r-- | doc/user/application_security/coverage_fuzzing/index.md | 2 | ||||
-rw-r--r-- | doc/user/application_security/dast/checks/598.1.md | 31 | ||||
-rw-r--r-- | doc/user/application_security/dast/checks/index.md | 1 | ||||
-rw-r--r-- | doc/user/application_security/iac_scanning/index.md | 7 | ||||
-rw-r--r-- | lib/backup/gitaly_backup.rb | 12 | ||||
-rw-r--r-- | lib/backup/manager.rb | 7 | ||||
-rw-r--r-- | locale/gitlab.pot | 21 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_form_spec.js | 14 | ||||
-rw-r--r-- | spec/lib/backup/gitaly_backup_spec.rb | 99 | ||||
-rw-r--r-- | spec/tasks/gitlab/backup_rake_spec.rb | 20 |
15 files changed, 343 insertions, 51 deletions
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index f39f9751c83..5fcf9514708 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -3,6 +3,7 @@ import { GlModal, GlAlert } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { formType } from '../constants'; import createBoardMutation from '../graphql/board_create.mutation.graphql'; @@ -15,6 +16,7 @@ const boardDefaults = { name: '', labels: [], milestone: {}, + iterationCadence: {}, iteration: {}, assignee: {}, weight: null, @@ -41,6 +43,7 @@ export default { BoardConfigurationOptions, GlAlert, }, + mixins: [glFeatureFlagMixin()], inject: { fullPath: { default: '', @@ -231,9 +234,12 @@ export default { this.board = { ...boardDefaults, ...this.currentBoard }; } }, - setIteration(iterationId) { + setIteration(iteration) { + if (this.glFeatures.iterationCadences) { + this.board.iterationCadenceId = iteration.iterationCadenceId; + } this.$set(this.board, 'iteration', { - id: iterationId, + id: iteration.id, }); }, setBoardLabels(labels) { diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue index 153b0981813..2a79ccc2648 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue @@ -1,22 +1,28 @@ <script> import { + GlIcon, GlLoadingIcon, GlDropdown, GlDropdownForm, GlDropdownDivider, GlDropdownItem, + GlDropdownSectionHeader, GlSearchBoxByType, } from '@gitlab/ui'; import { __ } from '~/locale'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; export default { components: { + GlIcon, GlLoadingIcon, GlDropdown, GlDropdownForm, GlDropdownDivider, GlDropdownItem, + GlDropdownSectionHeader, GlSearchBoxByType, + TooltipOnTruncate, }, props: { selectText: { @@ -39,6 +45,11 @@ export default { required: false, default: () => [], }, + groupedOptions: { + type: Array, + required: false, + default: () => [], + }, isLoading: { type: Boolean, required: false, @@ -79,11 +90,7 @@ export default { if (Array.isArray(this.selected)) { return this.selected.some((label) => label.title === option.title); } - return ( - this.selected && - ((option.name && this.selected.name === option.name) || - (option.title && this.selected.title === option.title)) - ); + return this.selected && option.id && this.selected.id === option.id; }, showDropdown() { this.$refs.dropdown.show(); @@ -101,6 +108,9 @@ export default { // TODO: this has some knowledge of the context where the component is used. We could later rework it. return option.username || null; }, + optionKey(option) { + return option.key ? option.key : option.id; + }, }, i18n: { noMatchingResults: __('No matching results'), @@ -154,10 +164,10 @@ export default { </template> <gl-dropdown-item v-for="option in options" - :key="option.id" + :key="optionKey(option)" :is-checked="isSelected(option)" - :is-check-centered="true" - :is-check-item="true" + is-check-centered + is-check-item :avatar-url="avatarUrl(option)" :secondary-text="secondaryText(option)" data-testid="unselected-option" @@ -167,6 +177,36 @@ export default { {{ option.title }} </slot> </gl-dropdown-item> + <template v-for="(optionGroup, index) in groupedOptions"> + <gl-dropdown-divider v-if="index !== 0" :key="index" /> + <gl-dropdown-section-header :key="optionGroup.id"> + <div class="gl-display-flex gl-max-w-full"> + <tooltip-on-truncate + :title="optionGroup.title" + class="gl-text-truncate gl-flex-grow-1" + > + {{ optionGroup.title }} + </tooltip-on-truncate> + <span v-if="optionGroup.secondaryText" class="gl-float-right gl-font-weight-normal"> + <gl-icon name="clock" class="gl-mr-2" /> + {{ optionGroup.secondaryText }} + </span> + </div> + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="option in optionGroup.options" + :key="optionKey(option)" + :is-checked="isSelected(option)" + is-check-centered + is-check-item + data-testid="unselected-option" + @click="selectOption(option)" + > + <slot name="item" :item="option"> + {{ option.title }} + </slot> + </gl-dropdown-item> + </template> <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!"> {{ $options.i18n.noMatchingResults }} </gl-dropdown-item> diff --git a/config/feature_flags/development/incremental_repository_backup.yml b/config/feature_flags/development/incremental_repository_backup.yml new file mode 100644 index 00000000000..d9eb97ba327 --- /dev/null +++ b/config/feature_flags/development/incremental_repository_backup.yml @@ -0,0 +1,8 @@ +--- +name: incremental_repository_backup +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79589 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355945 +milestone: '14.9' +type: development +group: group::gitaly +default_enabled: false diff --git a/doc/api/environments.md b/doc/api/environments.md index 8188e0e7b85..40d161485ff 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -35,7 +35,90 @@ Example response: "name": "review/fix-foo", "slug": "review-fix-foo-dfjre3", "external_url": "https://review-fix-foo-dfjre3.gitlab.example.com", - "state": "available" + "state": "available", + "created_at": "2019-05-25T18:55:13.252Z", + "updated_at": "2019-05-27T18:55:13.252Z", + "enable_advanced_logs_querying": false, + "logs_api_path": "/project/-/logs/k8s.json?environment_name=review%2Ffix-foo", + "last_deployment": { + "id": 100, + "iid": 34, + "ref": "fdroid", + "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab", + "created_at": "2019-03-25T18:55:13.252Z", + "status": "success", + "user": { + "id": 1, + "name": "Administrator", + "state": "active", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "deployable": { + "id": 710, + "status": "success", + "stage": "deploy", + "name": "staging", + "ref": "fdroid", + "tag": false, + "coverage": null, + "created_at": "2019-03-25T18:55:13.215Z", + "started_at": "2019-03-25T12:54:50.082Z", + "finished_at": "2019-03-25T18:55:13.216Z", + "duration": 21623.13423, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.dev/root", + "created_at": "2015-12-21T13:14:24.077Z", + "bio": null, + "location": null, + "public_email": "", + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": null + }, + "commit": { + "id": "416d8ea11849050d3d1f5104cf8cf51053e790ab", + "short_id": "416d8ea1", + "created_at": "2016-01-02T15:39:18.000Z", + "parent_ids": [ + "e9a4449c95c64358840902508fc827f1a2eab7df" + ], + "title": "Removed fabric to fix #40", + "message": "Removed fabric to fix #40\n", + "author_name": "Administrator", + "author_email": "admin@example.com", + "authored_date": "2016-01-02T15:39:18.000Z", + "committer_name": "Administrator", + "committer_email": "admin@example.com", + "committed_date": "2016-01-02T15:39:18.000Z" + }, + "pipeline": { + "id": 34, + "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab", + "ref": "fdroid", + "status": "success", + "web_url": "http://localhost:3000/Commit451/lab-coat/pipelines/34" + }, + "web_url": "http://localhost:3000/Commit451/lab-coat/-/jobs/710", + "artifacts": [ + { + "file_type": "trace", + "size": 1305, + "filename": "job.log", + "file_format": null + } + ], + "runner": null, + "artifacts_expire_at": null + } } ] ``` @@ -66,6 +149,8 @@ Example of response "state": "available", "created_at": "2019-05-25T18:55:13.252Z", "updated_at": "2019-05-27T18:55:13.252Z", + "enable_advanced_logs_querying": false, + "logs_api_path": "/project/-/logs/k8s.json?environment_name=review%2Ffix-foo", "last_deployment": { "id": 100, "iid": 34, diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 39162230cc2..cef52cc61d7 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -1854,3 +1854,22 @@ To enable it: ```ruby Feature.enable(:gitaly_backup) ``` + +### Incremental repository backups + +> Introduced in GitLab 14.9 [with a flag](../administration/feature_flags.md) named `incremental_repository_backup`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `incremental_repository_backup`. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +Incremental backups can be faster than full backups because they only pack changes since the last backup into the backup +bundle for each repository. Because incremental backups require access to the previous backup, you can't use incremental +backups with tar files. + +To create an incremental backup, run: + +```shell +sudo gitlab-backup create SKIP=tar INCREMENTAL=yes +``` diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md index a893106f52a..14e98766f0f 100644 --- a/doc/user/application_security/coverage_fuzzing/index.md +++ b/doc/user/application_security/coverage_fuzzing/index.md @@ -121,7 +121,7 @@ Use the following variables to configure coverage-guided fuzz testing in your CI | `COVFUZZ_URL_PREFIX` | Path to the `gitlab-cov-fuzz` repository cloned for use with an offline environment. You should only change this value when using an offline environment. Default: `https://gitlab.com/gitlab-org/security-products/analyzers/gitlab-cov-fuzz/-/raw`. | | `COVFUZZ_USE_REGISTRY` | Set to `true` to have the corpus stored in the GitLab corpus registry. The variables `COVFUZZ_CORPUS_NAME` and `COVFUZZ_GITLAB_TOKEN` are required if this variable is set to `true`. Default: `false`. [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5017) in GitLab 14.8. | | `COVFUZZ_CORPUS_NAME` | Name of the corpus to be used in the job. [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5017) in GitLab 14.8. | -| `COVFUZZ_GITLAB_TOKEN` | Environment variable configured with [Personal Access Token](../../../user/profile/personal_access_tokens.md#create-a-personal-access-token) with API read/write access. [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5017) in GitLab 14.8. | +| `COVFUZZ_GITLAB_TOKEN` | Environment variable configured with [Personal Access Token](../../../user/profile/personal_access_tokens.md#create-a-personal-access-token) or [Project Access Token](../../../user/project/settings/project_access_tokens.md#create-a-project-access-token) with API read/write access. [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5017) in GitLab 14.8. | #### Seed corpus diff --git a/doc/user/application_security/dast/checks/598.1.md b/doc/user/application_security/dast/checks/598.1.md new file mode 100644 index 00000000000..817e20ec413 --- /dev/null +++ b/doc/user/application_security/dast/checks/598.1.md @@ -0,0 +1,31 @@ +--- +stage: Secure +group: Dynamic Analysis +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 +--- + +# Use of GET request method with sensitive query strings (session ID) + +## Description + +A session ID was identified in the request URL as well as a cookie value. Session +IDs should not be sent in GET requests as they maybe captured by proxy systems, stored in +browser history, or stored in log files. If an attacker were to get access to the session +ID they would potentially be able to gain access to the target account. + +## Remediation + +As request headers are rarely logged or captured by third party systems, ensure session ID +values are only sent in cookies (assigned via `Set-Cookie` response headers) and never sent +in the request URL. + +## Details + +| ID | Aggregated | CWE | Type | Risk | +|:---|:--------|:--------|:--------|:--------| +| 598.1 | true | 598 | Passive | Medium | + +## Links + +- [OWASP](https://owasp.org/www-community/vulnerabilities/Information_exposure_through_query_strings_in_url) +- [CWE](https://cwe.mitre.org/data/definitions/598.html) diff --git a/doc/user/application_security/dast/checks/index.md b/doc/user/application_security/dast/checks/index.md index 97224554723..435bc28c4aa 100644 --- a/doc/user/application_security/dast/checks/index.md +++ b/doc/user/application_security/dast/checks/index.md @@ -19,5 +19,6 @@ The [DAST browser-based crawler](../browser_based.md) provides a number of vulne | [16.6](16.6.md) | AspNetMvc header exposes version information | Low | Passive | | [200.1](200.1.md) | Exposure of sensitive information to an unauthorized actor (private IP address) | Low | Passive | | [548.1](548.1.md) | Exposure of information through directory listing | Low | Passive | +| [598.1](598.1.md) | Use of GET request method with sensitive query strings (session ID) | Medium | Passive | | [614.1](614.1.md) | Sensitive cookie without Secure attribute | Low | Passive | | [693.1](693.1.md) | Missing X-Content-Type-Options: nosniff | Low | Passive | diff --git a/doc/user/application_security/iac_scanning/index.md b/doc/user/application_security/iac_scanning/index.md index 884dc24e20f..b72f54b4493 100644 --- a/doc/user/application_security/iac_scanning/index.md +++ b/doc/user/application_security/iac_scanning/index.md @@ -76,7 +76,12 @@ To configure IaC Scanning for a project you can: ### Configure IaC Scanning manually To enable IaC Scanning you must [include](../../../ci/yaml/index.md#includetemplate) the -[`SAST-IaC.latest.gitlab-ci.yml template`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST-IaC.latest.gitlab-ci.yml) provided as part of your GitLab installation. +[`SAST-IaC.latest.gitlab-ci.yml template`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST-IaC.latest.gitlab-ci.yml) provided as part of your GitLab installation. Here is an example of how to include it: + +```yaml +include: + - template: Security/SAST-IaC.latest.gitlab-ci.yml +``` The included template creates IaC scanning jobs in your CI/CD pipeline and scans your project's configuration files for possible vulnerabilities. diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb index 149aa00c2ce..b688ff7f13b 100644 --- a/lib/backup/gitaly_backup.rb +++ b/lib/backup/gitaly_backup.rb @@ -9,10 +9,13 @@ module Backup # @param [StringIO] progress IO interface to output progress # @param [Integer] max_parallelism max parallelism when running backups # @param [Integer] storage_parallelism max parallelism per storage (is affected by max_parallelism) - def initialize(progress, max_parallelism: nil, storage_parallelism: nil) + # @param [String] backup_id unique identifier for the backup + def initialize(progress, max_parallelism: nil, storage_parallelism: nil, incremental: false, backup_id: nil) @progress = progress @max_parallelism = max_parallelism @storage_parallelism = storage_parallelism + @incremental = incremental + @backup_id = backup_id end def start(type, backup_repos_path) @@ -30,6 +33,13 @@ module Backup args = [] args += ['-parallel', @max_parallelism.to_s] if @max_parallelism args += ['-parallel-storage', @storage_parallelism.to_s] if @storage_parallelism + if Feature.enabled?(:incremental_repository_backup, default_enabled: :yaml) + args += ['-layout', 'pointer'] + if type == :create + args += ['-incremental'] if @incremental + args += ['-id', @backup_id] if @backup_id + end + end @input_stream, stdout, @thread = Open3.popen2(build_env, bin_path, command, '-path', backup_repos_path, *args) diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index a19fcd6fede..6e90824fce2 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -21,6 +21,7 @@ module Backup max_concurrency = ENV.fetch('GITLAB_BACKUP_MAX_CONCURRENCY', 1).to_i max_storage_concurrency = ENV.fetch('GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY', 1).to_i force = ENV['force'] == 'yes' + incremental = Gitlab::Utils.to_boolean(ENV['INCREMENTAL'], default: false) @definitions = definitions || { 'db' => TaskDefinition.new( @@ -32,7 +33,7 @@ module Backup destination_path: 'repositories', destination_optional: true, task: Repositories.new(progress, - strategy: repository_backup_strategy, + strategy: repository_backup_strategy(incremental), max_concurrency: max_concurrency, max_storage_concurrency: max_storage_concurrency) ), @@ -481,11 +482,11 @@ module Backup Gitlab.config.backup.upload.connection&.provider&.downcase == 'google' end - def repository_backup_strategy + def repository_backup_strategy(incremental) if Feature.enabled?(:gitaly_backup, default_enabled: :yaml) max_concurrency = ENV['GITLAB_BACKUP_MAX_CONCURRENCY'].presence max_storage_concurrency = ENV['GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY'].presence - Backup::GitalyBackup.new(progress, max_parallelism: max_concurrency, storage_parallelism: max_storage_concurrency) + Backup::GitalyBackup.new(progress, incremental: incremental, max_parallelism: max_concurrency, storage_parallelism: max_storage_concurrency) else Backup::GitalyRpcBackup.new(progress) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 95267f78373..e8075deb16c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5852,6 +5852,9 @@ msgstr "" msgid "BoardNewIssue|Select a project" msgstr "" +msgid "BoardScope|An error occurred while getting iterations. Please try again." +msgstr "" + msgid "BoardScope|An error occurred while getting milestones, please try again." msgstr "" @@ -5867,6 +5870,9 @@ msgstr "" msgid "BoardScope|Any assignee" msgstr "" +msgid "BoardScope|Any iteration" +msgstr "" + msgid "BoardScope|Any label" msgstr "" @@ -5876,24 +5882,39 @@ msgstr "" msgid "BoardScope|Choose labels" msgstr "" +msgid "BoardScope|Current iteration" +msgstr "" + msgid "BoardScope|Edit" msgstr "" +msgid "BoardScope|Iteration" +msgstr "" + msgid "BoardScope|Labels" msgstr "" msgid "BoardScope|Milestone" msgstr "" +msgid "BoardScope|No iteration" +msgstr "" + msgid "BoardScope|No milestone" msgstr "" +msgid "BoardScope|Search iterations" +msgstr "" + msgid "BoardScope|Search milestones" msgstr "" msgid "BoardScope|Select assignee" msgstr "" +msgid "BoardScope|Select iteration" +msgstr "" + msgid "BoardScope|Select labels" msgstr "" diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 5678da2a246..c976ba7525b 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -1,6 +1,6 @@ import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import setWindowLocation from 'helpers/set_window_location_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import BoardForm from '~/boards/components/board_form.vue'; @@ -22,6 +22,8 @@ const currentBoard = { labels: [], milestone: {}, assignee: {}, + iteration: {}, + iterationCadence: {}, weight: null, hideBacklogList: false, hideClosedList: false, @@ -37,11 +39,11 @@ describe('BoardForm', () => { let wrapper; let mutate; - const findModal = () => wrapper.find(GlModal); + const findModal = () => wrapper.findComponent(GlModal); const findModalActionPrimary = () => findModal().props('actionPrimary'); - const findForm = () => wrapper.find('[data-testid="board-form"]'); - const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]'); - const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]'); + const findForm = () => wrapper.findByTestId('board-form'); + const findFormWrapper = () => wrapper.findByTestId('board-form-wrapper'); + const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message'); const findInput = () => wrapper.find('#board-new-name'); const store = createStore({ @@ -52,7 +54,7 @@ describe('BoardForm', () => { }); const createComponent = (props, data) => { - wrapper = shallowMount(BoardForm, { + wrapper = shallowMountExtended(BoardForm, { propsData: { ...defaultProps, ...props }, data() { return { diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index 84ee75e27ac..f5295c2b04c 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Backup::GitalyBackup do let(:max_parallelism) { nil } let(:storage_parallelism) { nil } let(:destination) { File.join(Gitlab.config.backup.path, 'repositories') } + let(:backup_id) { '20220101' } let(:progress) do Tempfile.new('progress').tap do |progress| @@ -24,7 +25,7 @@ RSpec.describe Backup::GitalyBackup do progress.close end - subject { described_class.new(progress, max_parallelism: max_parallelism, storage_parallelism: storage_parallelism) } + subject { described_class.new(progress, max_parallelism: max_parallelism, storage_parallelism: storage_parallelism, backup_id: backup_id) } context 'unknown' do it 'fails to start unknown' do @@ -41,7 +42,7 @@ RSpec.describe Backup::GitalyBackup do project_snippet = create(:project_snippet, :repository, project: project) personal_snippet = create(:personal_snippet, :repository, author: project.first_owner) - expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything).and_call_original + expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-layout', 'pointer', '-id', backup_id).and_call_original subject.start(:create, destination) subject.enqueue(project, Gitlab::GlRepository::PROJECT) @@ -51,18 +52,18 @@ RSpec.describe Backup::GitalyBackup do subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) subject.finish! - expect(File).to exist(File.join(destination, project.disk_path + '.bundle')) - expect(File).to exist(File.join(destination, project.disk_path + '.wiki.bundle')) - expect(File).to exist(File.join(destination, project.disk_path + '.design.bundle')) - expect(File).to exist(File.join(destination, personal_snippet.disk_path + '.bundle')) - expect(File).to exist(File.join(destination, project_snippet.disk_path + '.bundle')) + expect(File).to exist(File.join(destination, project.disk_path, backup_id, '001.bundle')) + expect(File).to exist(File.join(destination, project.disk_path + '.wiki', backup_id, '001.bundle')) + expect(File).to exist(File.join(destination, project.disk_path + '.design', backup_id, '001.bundle')) + expect(File).to exist(File.join(destination, personal_snippet.disk_path, backup_id, '001.bundle')) + expect(File).to exist(File.join(destination, project_snippet.disk_path, backup_id, '001.bundle')) end context 'parallel option set' do let(:max_parallelism) { 3 } it 'passes parallel option through' do - expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-parallel', '3').and_call_original + expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-parallel', '3', '-layout', 'pointer', '-id', backup_id).and_call_original subject.start(:create, destination) subject.finish! @@ -73,7 +74,7 @@ RSpec.describe Backup::GitalyBackup do let(:storage_parallelism) { 3 } it 'passes parallel option through' do - expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-parallel-storage', '3').and_call_original + expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-parallel-storage', '3', '-layout', 'pointer', '-id', backup_id).and_call_original subject.start(:create, destination) subject.finish! @@ -86,6 +87,36 @@ RSpec.describe Backup::GitalyBackup do subject.start(:create, destination) expect { subject.finish! }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1') end + + context 'feature flag incremental_repository_backup disabled' do + before do + stub_feature_flags(incremental_repository_backup: false) + end + + it 'creates repository bundles', :aggregate_failures do + # Add data to the wiki, design repositories, and snippets, so they will be included in the dump. + create(:wiki_page, container: project) + create(:design, :with_file, issue: create(:issue, project: project)) + project_snippet = create(:project_snippet, :repository, project: project) + personal_snippet = create(:personal_snippet, :repository, author: project.first_owner) + + expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything).and_call_original + + subject.start(:create, destination) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.finish! + + expect(File).to exist(File.join(destination, project.disk_path + '.bundle')) + expect(File).to exist(File.join(destination, project.disk_path + '.wiki.bundle')) + expect(File).to exist(File.join(destination, project.disk_path + '.design.bundle')) + expect(File).to exist(File.join(destination, personal_snippet.disk_path + '.bundle')) + expect(File).to exist(File.join(destination, project_snippet.disk_path + '.bundle')) + end + end end context 'hashed storage' do @@ -113,7 +144,7 @@ RSpec.describe Backup::GitalyBackup do end it 'passes through SSL envs' do - expect(Open3).to receive(:popen2).with(ssl_env, anything, 'create', '-path', anything).and_call_original + expect(Open3).to receive(:popen2).with(ssl_env, anything, 'create', '-path', anything, '-layout', 'pointer', '-id', backup_id).and_call_original subject.start(:create, destination) subject.finish! @@ -138,7 +169,7 @@ RSpec.describe Backup::GitalyBackup do copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle') copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle') - expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything).and_call_original + expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer').and_call_original subject.start(:restore, destination) subject.enqueue(project, Gitlab::GlRepository::PROJECT) @@ -150,18 +181,18 @@ RSpec.describe Backup::GitalyBackup do collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } - expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) - expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) - expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) - expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) - expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1']) + expect(collect_commit_shas.call(project.repository)).to match_array(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) + expect(collect_commit_shas.call(project.wiki.repository)).to match_array(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) + expect(collect_commit_shas.call(project.design_repository)).to match_array(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) + expect(collect_commit_shas.call(personal_snippet.repository)).to match_array(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) + expect(collect_commit_shas.call(project_snippet.repository)).to match_array(['6e44ba56a4748be361a841e759c20e421a1651a1']) end context 'parallel option set' do let(:max_parallelism) { 3 } it 'passes parallel option through' do - expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-parallel', '3').and_call_original + expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-parallel', '3', '-layout', 'pointer').and_call_original subject.start(:restore, destination) subject.finish! @@ -172,13 +203,45 @@ RSpec.describe Backup::GitalyBackup do let(:storage_parallelism) { 3 } it 'passes parallel option through' do - expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-parallel-storage', '3').and_call_original + expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-parallel-storage', '3', '-layout', 'pointer').and_call_original subject.start(:restore, destination) subject.finish! end end + context 'feature flag incremental_repository_backup disabled' do + before do + stub_feature_flags(incremental_repository_backup: false) + end + + it 'restores from repository bundles', :aggregate_failures do + copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle') + copy_bundle_to_backup_path('wiki_repo.bundle', project.disk_path + '.wiki.bundle') + copy_bundle_to_backup_path('design_repo.bundle', project.disk_path + '.design.bundle') + copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle') + copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle') + + expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything).and_call_original + + subject.start(:restore, destination) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.finish! + + collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } + + expect(collect_commit_shas.call(project.repository)).to match_array(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) + expect(collect_commit_shas.call(project.wiki.repository)).to match_array(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) + expect(collect_commit_shas.call(project.design_repository)).to match_array(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) + expect(collect_commit_shas.call(personal_snippet.repository)).to match_array(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) + expect(collect_commit_shas.call(project_snippet.repository)).to match_array(['6e44ba56a4748be361a841e759c20e421a1651a1']) + end + end + it 'raises when the exit code not zero' do expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false')) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 3b64034fc2d..df9f2a0d3bb 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -176,8 +176,8 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do expect(exit_status).to eq(0) expect(tar_contents).to match(user_backup_path) - expect(tar_contents).to match("#{user_backup_path}/custom_hooks.tar") - expect(tar_contents).to match("#{user_backup_path}.bundle") + expect(tar_contents).to match("#{user_backup_path}/.+/001.custom_hooks.tar") + expect(tar_contents).to match("#{user_backup_path}/.+/001.bundle") end it 'restores files correctly' do @@ -360,14 +360,14 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do expect(exit_status).to eq(0) [ - "#{project_a.disk_path}.bundle", - "#{project_a.disk_path}.wiki.bundle", - "#{project_a.disk_path}.design.bundle", - "#{project_b.disk_path}.bundle", - "#{project_snippet_a.disk_path}.bundle", - "#{project_snippet_b.disk_path}.bundle" + "#{project_a.disk_path}/.+/001.bundle", + "#{project_a.disk_path}.wiki/.+/001.bundle", + "#{project_a.disk_path}.design/.+/001.bundle", + "#{project_b.disk_path}/.+/001.bundle", + "#{project_snippet_a.disk_path}/.+/001.bundle", + "#{project_snippet_b.disk_path}/.+/001.bundle" ].each do |repo_name| - expect(tar_lines.grep(/#{repo_name}/).size).to eq 1 + expect(tar_lines).to include(a_string_matching(repo_name)) end end @@ -428,7 +428,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do expect(::Backup::Repositories).to receive(:new) .with(anything, strategy: anything, max_concurrency: 5, max_storage_concurrency: 2) .and_call_original - expect(::Backup::GitalyBackup).to receive(:new).with(anything, max_parallelism: 5, storage_parallelism: 2).and_call_original + expect(::Backup::GitalyBackup).to receive(:new).with(anything, max_parallelism: 5, storage_parallelism: 2, incremental: false).and_call_original expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process end |