diff options
42 files changed, 578 insertions, 311 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 8062460f052..cf04669ba43 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,7 +1,13 @@ import { sortBy, cloneDeep } from 'lodash'; import { TYPE_BOARD, TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants'; import { isGid, convertToGraphQLId } from '~/graphql_shared/utils'; -import { ListType, MilestoneIDs, AssigneeFilterType, MilestoneFilterType } from './constants'; +import { + ListType, + MilestoneIDs, + AssigneeFilterType, + MilestoneFilterType, + boardQuery, +} from './constants'; export function getMilestone() { return null; @@ -305,6 +311,10 @@ export function transformBoardConfig() { return ''; } +export function getBoardQuery(boardType) { + return boardQuery[boardType].query; +} + export default { getMilestone, formatIssue, diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue index 970e3509d20..d41fc1e9300 100644 --- a/app/assets/javascripts/boards/components/board_app.vue +++ b/app/assets/javascripts/boards/components/board_app.vue @@ -11,7 +11,12 @@ export default { BoardSettingsSidebar, BoardTopBar, }, - inject: ['fullBoardId'], + inject: ['initialBoardId'], + data() { + return { + boardId: this.initialBoardId, + }; + }, computed: { ...mapGetters(['isSidebarOpen']), }, @@ -21,13 +26,18 @@ export default { destroyed() { window.removeEventListener('popstate', refreshCurrentPage); }, + methods: { + switchBoard(id) { + this.boardId = id; + }, + }, }; </script> <template> <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }"> - <board-top-bar /> - <board-content :board-id="fullBoardId" /> + <board-top-bar :board-id="boardId" @switchBoard="switchBoard" /> + <board-content :board-id="boardId" /> <board-settings-sidebar /> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue index 368feba9a44..2e20ed70bb0 100644 --- a/app/assets/javascripts/boards/components/board_top_bar.vue +++ b/app/assets/javascripts/boards/components/board_top_bar.vue @@ -2,6 +2,7 @@ import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue'; import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue'; +import { getBoardQuery } from 'ee_else_ce/boards/boards_util'; import ConfigToggle from './config_toggle.vue'; import NewBoardButton from './new_board_button.vue'; import ToggleFocus from './toggle_focus.vue'; @@ -19,7 +20,46 @@ export default { EpicBoardFilteredSearch: () => import('ee_component/boards/components/epic_filtered_search.vue'), }, - inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn', 'isIssueBoard'], + inject: [ + 'swimlanesFeatureAvailable', + 'canAdminList', + 'isSignedIn', + 'isIssueBoard', + 'fullPath', + 'boardType', + 'isEpicBoard', + 'isApolloBoard', + ], + props: { + boardId: { + type: String, + required: true, + }, + }, + data() { + return { + board: {}, + }; + }, + apollo: { + board: { + query() { + return getBoardQuery(this.boardType, this.isEpicBoard); + }, + variables() { + return { + fullPath: this.fullPath, + boardId: this.boardId, + }; + }, + skip() { + return !this.isApolloBoard; + }, + update(data) { + return data.workspace.board; + }, + }, + }, }; </script> @@ -31,7 +71,7 @@ export default { <div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0 gl-mb-3 gl-w-full" > - <boards-selector /> + <boards-selector :board-apollo="board" @switchBoard="$emit('switchBoard', $event)" /> <new-board-button /> <issue-board-filtered-search v-if="isIssueBoard" /> <epic-board-filtered-search v-else /> diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index d26aeb69dd5..33a82ab3827 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -51,6 +51,7 @@ export default { 'weights', 'boardType', 'isGroupBoard', + 'isApolloBoard', ], props: { throttleDuration: { @@ -58,6 +59,11 @@ export default { default: 200, required: false, }, + boardApollo: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { return { @@ -77,6 +83,9 @@ export default { computed: { ...mapState(['board', 'isBoardLoading']), + boardToUse() { + return this.isApolloBoard ? this.boardApollo : this.board; + }, parentType() { return this.boardType; }, @@ -116,7 +125,7 @@ export default { this.scrollFadeInitialized = false; this.$nextTick(this.setScrollFade); }, - board(newBoard) { + boardToUse(newBoard) { document.title = newBoard.name; }, }, @@ -210,9 +219,14 @@ export default { boardType: this.boardType, }); }, + fullBoardId(boardId) { + return fullBoardId(boardId); + }, async switchBoard(boardId, e) { if (isMetaKey(e)) { window.open(`${this.boardBaseUrl}/${boardId}`, '_blank'); + } else if (this.isApolloBoard) { + this.$emit('switchBoard', this.fullBoardId(boardId)); } else { this.unsetActiveId(); this.fetchCurrentBoard(boardId); @@ -235,7 +249,7 @@ export default { toggle-class="dropdown-menu-toggle" menu-class="flex-column dropdown-extended-height" :loading="isBoardLoading" - :text="board.name" + :text="boardToUse.name" @show="loadBoards" > <p class="gl-dropdown-header-top" @mousedown.prevent> @@ -333,7 +347,7 @@ export default { :can-admin-board="canAdminBoard" :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" :weights="weights" - :current-board="board" + :current-board="boardToUse" :current-page="currentPage" @cancel="cancel" /> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 91b7f5004ad..7a5ef01606f 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -7,6 +7,8 @@ import updateBoardListMutation from './graphql/board_list_update.mutation.graphq import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql'; import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql'; +import groupBoardQuery from './graphql/group_board.query.graphql'; +import projectBoardQuery from './graphql/project_board.query.graphql'; /* eslint-disable-next-line @gitlab/require-i18n-strings */ export const AssigneeIdParamValues = ['Any', 'None']; @@ -59,6 +61,15 @@ export const INCIDENT = 'INCIDENT'; export const flashAnimationDuration = 2000; +export const boardQuery = { + [BoardType.group]: { + query: groupBoardQuery, + }, + [BoardType.project]: { + query: projectBoardQuery, + }, +}; + export const listsQuery = { [issuableTypes.issue]: { query: boardListsQuery, diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 968832a092d..a44684ab12d 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -24,6 +24,7 @@ const apolloProvider = new VueApollo({ function mountBoardApp(el) { const { boardId, groupId, fullPath, rootPath } = el.dataset; + const isApolloBoard = window.gon?.features?.apolloBoards; const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true }); @@ -33,20 +34,22 @@ function mountBoardApp(el) { const boardType = el.dataset.parent; - store.dispatch('fetchBoard', { - fullPath, - fullBoardId: fullBoardId(boardId), - boardType, - }); + if (!isApolloBoard) { + store.dispatch('fetchBoard', { + fullPath, + fullBoardId: fullBoardId(boardId), + boardType, + }); - store.dispatch('setInitialBoardData', { - boardId, - fullBoardId: fullBoardId(boardId), - fullPath, - boardType, - disabled: parseBoolean(el.dataset.disabled) || true, - issuableType: issuableTypes.issue, - }); + store.dispatch('setInitialBoardData', { + boardId, + fullBoardId: fullBoardId(boardId), + fullPath, + boardType, + disabled: parseBoolean(el.dataset.disabled) || true, + issuableType: issuableTypes.issue, + }); + } // eslint-disable-next-line no-new new Vue({ @@ -55,8 +58,8 @@ function mountBoardApp(el) { store, apolloProvider, provide: { - isApolloBoard: window.gon?.features?.apolloBoards, - fullBoardId: fullBoardId(boardId), + isApolloBoard, + initialBoardId: fullBoardId(boardId), disabled: parseBoolean(el.dataset.disabled), groupId: Number(groupId), rootPath, diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index cebf73ef8e5..20fb2b1aa94 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -18,10 +18,6 @@ const VARIANT_TIP = 'tip'; const FLASH_CLOSED_EVENT = 'flashClosed'; -const getCloseEl = (flashEl) => { - return flashEl.querySelector('.js-close-icon'); -}; - const hideFlash = (flashEl, fadeTransition = true) => { if (fadeTransition) { Object.assign(flashEl.style, { @@ -48,12 +44,6 @@ const hideFlash = (flashEl, fadeTransition = true) => { if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); }; -const addDismissFlashClickListener = (flashEl, fadeTransition) => { - // There are some flash elements which do not have a closeEl. - // https://gitlab.com/gitlab-org/gitlab/blob/763426ef344488972eb63ea5be8744e0f8459e6b/ee/app/views/layouts/header/_read_only_banner.html.haml - getCloseEl(flashEl)?.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); -}; - /** * Render an alert at the top of the page, or, optionally an * arbitrary existing container. This alert is always dismissible. @@ -183,7 +173,6 @@ const createAlert = function createAlert({ export { hideFlash, - addDismissFlashClickListener, FLASH_TYPES, FLASH_CLOSED_EVENT, createAlert, diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index fd5c4abe729..4c715c4993f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -16,7 +16,6 @@ import * as tooltips from '~/tooltips'; import { initPrefetchLinks } from '~/lib/utils/navigation_utility'; import { logHelloDeferred } from 'jh_else_ce/lib/logger/hello_deferred'; import initAlertHandler from './alert_handler'; -import { addDismissFlashClickListener } from './flash'; import initTodoToggle from './header'; import initLayoutNav from './layout_nav'; import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; @@ -253,16 +252,6 @@ $('form.filter-form').on('submit', function filterFormSubmitCallback(event) { visitUrl(action); }); -const flashContainer = document.querySelector('.flash-container'); - -if (flashContainer && flashContainer.children.length) { - flashContainer - .querySelectorAll('.flash-alert, .flash-notice, .flash-success') - .forEach((flashEl) => { - addDismissFlashClickListener(flashEl); - }); -} - // initialize field errors $('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form)); diff --git a/lib/backup/database.rb b/lib/backup/database.rb index cf19b4fa8ff..61dd6033eb0 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -6,7 +6,7 @@ module Backup class Database < Task extend ::Gitlab::Utils::Override include Backup::Helper - attr_reader :force, :config + attr_reader :force IGNORED_ERRORS = [ # Ignore warnings @@ -18,98 +18,108 @@ module Backup ].freeze IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze - def initialize(database_name, progress, force:) + def initialize(progress, force:) super(progress) - @database_name = database_name - @config = base_model.connection_db_config.configuration_hash @force = force end override :dump - def dump(db_file_name, backup_id) - FileUtils.mkdir_p(File.dirname(db_file_name)) - FileUtils.rm_f(db_file_name) - compress_rd, compress_wr = IO.pipe - compress_pid = spawn(gzip_cmd, in: compress_rd, out: [db_file_name, 'w', 0600]) - compress_rd.close - - dump_pid = - case config[:adapter] - when "postgresql" then - progress.print "Dumping PostgreSQL database #{database} ... " - pg_env - pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. - pgsql_args << '--if-exists' - - if Gitlab.config.backup.pg_schema - pgsql_args << '-n' - pgsql_args << Gitlab.config.backup.pg_schema + def dump(destination_dir, backup_id) + snapshot_ids = base_models_for_backup.each_with_object({}) do |(database_name, base_model), snapshot_ids| + base_model.connection.begin_transaction(isolation: :repeatable_read) - Gitlab::Database::EXTRA_SCHEMAS.each do |schema| - pgsql_args << '-n' - pgsql_args << schema.to_s - end - end + snapshot_ids[database_name] = + base_model.connection.execute("SELECT pg_export_snapshot() as snapshot_id;").first['snapshot_id'] + end + + FileUtils.mkdir_p(destination_dir) + + snapshot_ids.each do |database_name, snapshot_id| + base_model = base_models_for_backup[database_name] + + config = base_model.connection_db_config.configuration_hash + + db_file_name = file_name(destination_dir, database_name) + FileUtils.rm_f(db_file_name) + + pg_database = config[:database] - Process.spawn('pg_dump', *pgsql_args, database, out: compress_wr) + progress.print "Dumping PostgreSQL database #{pg_database} ... " + pg_env(config) + pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. + pgsql_args << '--if-exists' + pgsql_args << "--snapshot=#{snapshot_ids[database_name]}" + + if Gitlab.config.backup.pg_schema + pgsql_args << '-n' + pgsql_args << Gitlab.config.backup.pg_schema + + Gitlab::Database::EXTRA_SCHEMAS.each do |schema| + pgsql_args << '-n' + pgsql_args << schema.to_s + end end - compress_wr.close - success = [compress_pid, dump_pid].all? do |pid| - Process.waitpid(pid) - $?.success? - end + success = Backup::Dump::Postgres.new.dump(pg_database, db_file_name, pgsql_args) + + base_model.connection.rollback_transaction - report_success(success) - progress.flush + raise DatabaseBackupError.new(config, db_file_name) unless success - raise DatabaseBackupError.new(config, db_file_name) unless success + report_success(success) + progress.flush + end end override :restore - def restore(db_file_name) - unless File.exist?(db_file_name) - raise(Backup::Error, "Source database file does not exist #{db_file_name}") if main_database? + def restore(destination_dir) + base_models_for_backup.each do |database_name, base_model| + config = base_model.connection_db_config.configuration_hash - progress.puts "Source backup for the database #{@database_name} doesn't exist. Skipping the task" - return - end + db_file_name = file_name(destination_dir, database_name) + database = config[:database] - unless force - progress.puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) - sleep(5) - end + unless File.exist?(db_file_name) + raise(Backup::Error, "Source database file does not exist #{db_file_name}") if main_database?(database_name) - # Drop all tables Load the schema to ensure we don't have any newer tables - # hanging out from a failed upgrade - puts_time 'Cleaning the database ... '.color(:blue) - Rake::Task['gitlab:db:drop_tables'].invoke - puts_time 'done'.color(:green) - - decompress_rd, decompress_wr = IO.pipe - decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name) - decompress_wr.close - - status, @errors = - case config[:adapter] - when "postgresql" then - progress.print "Restoring PostgreSQL database #{database} ... " - pg_env - execute_and_track_errors(pg_restore_cmd, decompress_rd) + progress.puts "Source backup for the database #{database_name} doesn't exist. Skipping the task" + return false end - decompress_rd.close - Process.waitpid(decompress_pid) - success = $?.success? && status.success? + unless force + progress.puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) + sleep(5) + end - if @errors.present? - progress.print "------ BEGIN ERRORS -----\n".color(:yellow) - progress.print @errors.join.color(:yellow) - progress.print "------ END ERRORS -------\n".color(:yellow) - end + # Drop all tables Load the schema to ensure we don't have any newer tables + # hanging out from a failed upgrade + drop_tables(database_name) + + decompress_rd, decompress_wr = IO.pipe + decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name) + decompress_wr.close + + status, @errors = + case config[:adapter] + when "postgresql" then + progress.print "Restoring PostgreSQL database #{database} ... " + pg_env(config) + execute_and_track_errors(pg_restore_cmd(database), decompress_rd) + end + decompress_rd.close + + Process.waitpid(decompress_pid) + success = $?.success? && status.success? - report_success(success) - raise Backup::Error, 'Restore failed' unless success + if @errors.present? + progress.print "------ BEGIN ERRORS -----\n".color(:yellow) + progress.print @errors.join.color(:yellow) + progress.print "------ END ERRORS -------\n".color(:yellow) + end + + report_success(success) + raise Backup::Error, 'Restore failed' unless success + end end override :pre_restore_warning @@ -144,16 +154,22 @@ module Backup protected - def database - @config[:database] + def base_models_for_backup + @base_models_for_backup ||= Gitlab::Database.database_base_models_with_gitlab_shared end - def base_model - Gitlab::Database.database_base_models[@database_name] + def main_database?(database_name) + database_name.to_sym == :main end - def main_database? - @database_name == :main + def file_name(base_dir, database_name) + prefix = if database_name.to_sym != :main + "#{database_name}_" + else + '' + end + + File.join(base_dir, "#{prefix}database.sql.gz") end def ignore_error?(line) @@ -189,7 +205,7 @@ module Backup end end - def pg_env + def pg_env(config) args = { username: 'PGUSER', host: 'PGHOST', @@ -223,7 +239,20 @@ module Backup private - def pg_restore_cmd + def drop_tables(database_name) + if Rake::Task.task_defined? "gitlab:db:drop_tables:#{database_name}" + puts_time 'Cleaning the database ... '.color(:blue) + Rake::Task["gitlab:db:drop_tables:#{database_name}"].invoke + puts_time 'done'.color(:green) + elsif Gitlab::Database.database_base_models.one? + # In single database, we do not have rake tasks per database + puts_time 'Cleaning the database ... '.color(:blue) + Rake::Task["gitlab:db:drop_tables"].invoke + puts_time 'done'.color(:green) + end + end + + def pg_restore_cmd(database) ['psql', database] end end diff --git a/lib/backup/dump/postgres.rb b/lib/backup/dump/postgres.rb new file mode 100644 index 00000000000..1a5128b5a6b --- /dev/null +++ b/lib/backup/dump/postgres.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +module Backup + module Dump + class Postgres + include Backup::Helper + + FILE_PERMISSION = 0o600 + + def dump(database_name, output_file, pgsql_args) + compress_rd, compress_wr = IO.pipe + compress_pid = spawn(gzip_cmd, in: compress_rd, out: [output_file, 'w', FILE_PERMISSION]) + compress_rd.close + + dump_pid = Process.spawn('pg_dump', *pgsql_args, database_name, out: compress_wr) + compress_wr.close + + [compress_pid, dump_pid].all? do |pid| + Process.waitpid(pid) + $?.success? + end + end + end + end +end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index f8424f6250e..a7dddcf8619 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -22,7 +22,6 @@ module Backup :destination_optional, # `true` if the destination might not exist on a successful backup. :cleanup_path, # Path to remove after a successful backup. Uses `destination_path` when not specified. :task, - :task_group, keyword_init: true ) do def enabled? @@ -121,20 +120,11 @@ module Backup def build_definitions # rubocop:disable Metrics/AbcSize { - 'main_db' => TaskDefinition.new( - human_name: _('main_database'), - destination_path: 'db/database.sql.gz', + 'db' => TaskDefinition.new( + human_name: _('database'), + destination_path: 'db', cleanup_path: 'db', - task: build_db_task(:main), - task_group: 'db' - ), - 'ci_db' => TaskDefinition.new( - human_name: _('ci_database'), - destination_path: 'db/ci_database.sql.gz', - cleanup_path: 'db', - task: build_db_task(:ci), - enabled: Gitlab::Database.has_config?(:ci), - task_group: 'db' + task: build_db_task ), 'repositories' => TaskDefinition.new( human_name: _('repositories'), @@ -186,11 +176,10 @@ module Backup }.freeze end - def build_db_task(database_name) - return unless Gitlab::Database.has_config?(database_name) # It will be disabled for a single db setup - + def build_db_task force = Gitlab::Utils.to_boolean(ENV['force'], default: false) - Database.new(database_name, progress, force: force) + + Database.new(progress, force: force) end def build_repositories_task @@ -483,7 +472,7 @@ module Backup end def skipped?(item) - skipped.include?(item) || skipped.include?(definitions[item]&.task_group) + skipped.include?(item) end def skipped diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 6647a10898f..787df37a8f8 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -44,15 +44,13 @@ namespace :gitlab do namespace :db do task create: :gitlab_environment do lock do - Backup::Manager.new(progress).run_create_task('main_db') - Backup::Manager.new(progress).run_create_task('ci_db') + Backup::Manager.new(progress).run_create_task('db') end end task restore: :gitlab_environment do lock do - Backup::Manager.new(progress).run_restore_task('main_db') - Backup::Manager.new(progress).run_restore_task('ci_db') + Backup::Manager.new(progress).run_restore_task('db') end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 786733b32ee..5a60905422d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -49682,9 +49682,6 @@ msgstr "" msgid "ciReport|is loading, errors when loading results" msgstr "" -msgid "ci_database" -msgstr "" - msgid "closed" msgstr "" @@ -49768,6 +49765,9 @@ msgstr "" msgid "data" msgstr "" +msgid "database" +msgstr "" + msgid "date must not be after 9999-12-31" msgstr "" @@ -50208,9 +50208,6 @@ msgstr "" msgid "locked by %{path_lock_user_name} %{created_at}" msgstr "" -msgid "main_database" -msgstr "" - msgid "manual" msgstr "" diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 1b0695e4e60..d597c57ac1c 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -14,6 +14,10 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d let(:board_list_header) { first('[data-testid="board-list-header"]') } let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') } + before do + stub_feature_flags(apollo_boards: false) + end + context 'authorized user' do before do project.add_maintainer(user) diff --git a/spec/features/boards/reload_boards_on_browser_back_spec.rb b/spec/features/boards/reload_boards_on_browser_back_spec.rb index 0ca680c5ed5..036daee7655 100644 --- a/spec/features/boards/reload_boards_on_browser_back_spec.rb +++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feat context 'authorized user' do before do + stub_feature_flags(apollo_boards: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb index c3bb58df797..39485fe21a9 100644 --- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb +++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea let_it_be(:board_list) { create(:backlog_list, board: group_board) } before do + stub_feature_flags(apollo_boards: false) + load_board group_board_path(group, group_board) end diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb index 11ec38f637b..c451a97bed5 100644 --- a/spec/features/groups/board_spec.rb +++ b/spec/features/groups/board_spec.rb @@ -14,6 +14,8 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do let_it_be(:project) { create(:project_empty_repo, group: group) } before do + stub_feature_flags(apollo_boards: false) + group.add_maintainer(user) sign_in(user) @@ -60,6 +62,8 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do let_it_be(:issue2) { create(:issue, title: 'issue2', project: project2) } before do + stub_feature_flags(apollo_boards: false) + project1.add_guest(user) project2.add_reporter(user) diff --git a/spec/fixtures/database.sql.gz b/spec/fixtures/database.sql.gz Binary files differnew file mode 100644 index 00000000000..a98aa7c53f2 --- /dev/null +++ b/spec/fixtures/database.sql.gz diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json index c40befcf8ce..0d9c217afd1 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json @@ -7,7 +7,7 @@ "properties": { "type": { "enum": ["custom"] }, "label": { "type": "string" }, - "options": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json" } + "options": { "$ref": "custom_variable_options.json" } }, "additionalProperties": false } diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json index de72b947eed..bb78294e43e 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json @@ -5,7 +5,7 @@ "properties": { "values": { "type": "array", - "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_values.json" } + "items": { "$ref": "custom_variable_values.json" } } }, "additionalProperties": false diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json index 40453c61a65..f38f74ae13f 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json @@ -11,10 +11,10 @@ "priority": { "type": "number" }, "panel_groups": { "type": "array", - "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json" } + "items": { "$ref": "panel_groups.json" } }, - "templating": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json" }, - "links": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/links.json" } + "templating": { "$ref": "templating.json" }, + "links": { "$ref": "links.json" } }, "additionalProperties": false } diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json index b47b81fc103..a5228bc0888 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json @@ -6,7 +6,7 @@ "panel_groups": { "type": "array", "items": { - "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json" + "$ref": "embedded_panel_groups.json" } } }, diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json index 063016c22fd..b1c34ba1b86 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json @@ -5,7 +5,7 @@ "properties": { "panels": { "type": "array", - "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json" } + "items": { "$ref": "panels.json" } } }, "additionalProperties": false diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json index 145cc476d64..742708e60bd 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json @@ -15,8 +15,8 @@ "type": "string" }, "options": { - "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_options.json" + "$ref": "metric_label_values_variable_options.json" } }, "additionalProperties": false -}
\ No newline at end of file +} diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json index 392aa0e4480..a5a4428f2f3 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json @@ -9,7 +9,7 @@ "group": { "type": "string" }, "panels": { "type": "array", - "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json" } + "items": { "$ref": "panels.json" } }, "has_custom_metrics": { "type": "boolean" } }, diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json index 3224e7cfe3f..78369a7a055 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json @@ -11,11 +11,11 @@ "id": { "type": "string" }, "type": { "type": "string" }, "y_label": { "type": "string" }, - "y_axis": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/axis.json" }, + "y_axis": { "$ref": "axis.json" }, "max_value": { "type": "number" }, "metrics": { "type": "array", - "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json" } + "items": { "$ref": "metrics.json" } } }, "additionalProperties": false diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json index 439f7b6b044..c339edec128 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json @@ -3,7 +3,7 @@ "type": "object", "required": ["variables"], "properties": { - "variables": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json" } + "variables": { "$ref": "variables.json" } }, "additionalProperties": false } diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json index c4382326854..37ff4fdba5f 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json @@ -7,7 +7,7 @@ "properties": { "type": { "enum": ["text"] }, "label": { "type": "string" }, - "options": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_options.json" } + "options": { "$ref": "text_variable_options.json" } }, "additionalProperties": false } diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json index 1cf5ae2eaa4..73841d5bd82 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json @@ -4,14 +4,14 @@ "patternProperties": { "^[a-zA-Z0-9_]*$": { "anyOf": [ - { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json" }, + { "$ref": "text_variable_full_syntax.json" }, { "type": "string" }, { "type": "array", "items": { "type": "string" } }, - { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json" }, - { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json" } + { "$ref": "custom_variable_full_syntax.json" }, + { "$ref": "metric_label_values_variable_full_syntax.json" } ] } }, diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js index 872a67a71fb..12318fb5d16 100644 --- a/spec/frontend/boards/components/board_app_spec.js +++ b/spec/frontend/boards/components/board_app_spec.js @@ -27,7 +27,7 @@ describe('BoardApp', () => { wrapper = shallowMount(BoardApp, { store, provide: { - fullBoardId: 'gid://gitlab/Board/1', + initialBoardId: 'gid://gitlab/Board/1', }, }); }; diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js index af492145eb0..8258d9fe7f4 100644 --- a/spec/frontend/boards/components/board_top_bar_spec.js +++ b/spec/frontend/boards/components/board_top_bar_spec.js @@ -1,6 +1,8 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; +import createMockApollo from 'helpers/mock_apollo_helper'; import BoardTopBar from '~/boards/components/board_top_bar.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; @@ -9,11 +11,18 @@ import ConfigToggle from '~/boards/components/config_toggle.vue'; import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue'; import NewBoardButton from '~/boards/components/new_board_button.vue'; import ToggleFocus from '~/boards/components/toggle_focus.vue'; +import { BoardType } from '~/boards/constants'; + +import groupBoardQuery from '~/boards/graphql/group_board.query.graphql'; +import projectBoardQuery from '~/boards/graphql/project_board.query.graphql'; +import { mockProjectBoardResponse, mockGroupBoardResponse } from '../mock_data'; + +Vue.use(VueApollo); +Vue.use(Vuex); describe('BoardTopBar', () => { let wrapper; - - Vue.use(Vuex); + let mockApollo; const createStore = () => { return new Vuex.Store({ @@ -21,10 +30,22 @@ describe('BoardTopBar', () => { }); }; + const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse); + const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse); + const createComponent = ({ provide = {} } = {}) => { const store = createStore(); + mockApollo = createMockApollo([ + [projectBoardQuery, projectBoardQueryHandlerSuccess], + [groupBoardQuery, groupBoardQueryHandlerSuccess], + ]); + wrapper = shallowMount(BoardTopBar, { store, + apolloProvider: mockApollo, + props: { + boardId: 'gid://gitlab/Board/1', + }, provide: { swimlanesFeatureAvailable: false, canAdminList: false, @@ -33,7 +54,9 @@ describe('BoardTopBar', () => { boardType: 'group', releasesFetchPath: '/releases', isIssueBoard: true, + isEpicBoard: false, isGroupBoard: true, + isApolloBoard: false, ...provide, }, stubs: { IssueBoardFilteredSearch }, @@ -42,6 +65,7 @@ describe('BoardTopBar', () => { afterEach(() => { wrapper.destroy(); + mockApollo = null; }); describe('base template', () => { @@ -83,4 +107,26 @@ describe('BoardTopBar', () => { expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(true); }); }); + + describe('Apollo boards', () => { + it.each` + boardType | queryHandler | notCalledHandler + ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess} + ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess} + `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => { + createComponent({ + provide: { + boardType, + isProjectBoard: boardType === BoardType.project, + isGroupBoard: boardType === BoardType.group, + isApolloBoard: true, + }, + }); + + await nextTick(); + + expect(queryHandler).toHaveBeenCalled(); + expect(notCalledHandler).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 7b61ca5e6fd..dfd8d2351a6 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -108,6 +108,7 @@ describe('BoardsSelector', () => { boardType: isGroupBoard ? 'group' : 'project', isGroupBoard, isProjectBoard, + isApolloBoard: false, }, }); }; diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index df41eb05eae..0ab8a89bcca 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -50,6 +50,26 @@ export const mockBoard = { weight: 2, }; +export const mockProjectBoardResponse = { + data: { + workspace: { + id: 'gid://gitlab/Project/114', + board: mockBoard, + __typename: 'Project', + }, + }, +}; + +export const mockGroupBoardResponse = { + data: { + workspace: { + id: 'gid://gitlab/Group/114', + board: mockBoard, + __typename: 'Group', + }, + }, +}; + export const mockBoardConfig = { milestoneId: 'gid://gitlab/Milestone/114', milestoneTitle: '14.9', diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js index 2f0a52a9884..334117e0e3c 100644 --- a/spec/frontend/flash_spec.js +++ b/spec/frontend/flash_spec.js @@ -1,12 +1,6 @@ import * as Sentry from '@sentry/browser'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { - hideFlash, - addDismissFlashClickListener, - FLASH_CLOSED_EVENT, - createAlert, - VARIANT_WARNING, -} from '~/flash'; +import { hideFlash, FLASH_CLOSED_EVENT, createAlert, VARIANT_WARNING } from '~/flash'; jest.mock('@sentry/browser'); @@ -338,45 +332,4 @@ describe('Flash', () => { }); }); }); - - describe('addDismissFlashClickListener', () => { - let el; - - describe('with close icon', () => { - beforeEach(() => { - el = document.createElement('div'); - el.innerHTML = ` - <div class="flash-container"> - <div class="flash"> - <div class="close-icon js-close-icon"></div> - </div> - </div> - `; - }); - - it('removes global flash on click', () => { - addDismissFlashClickListener(el, false); - - el.querySelector('.js-close-icon').click(); - - expect(document.querySelector('.flash')).toBeNull(); - }); - }); - - describe('without close icon', () => { - beforeEach(() => { - el = document.createElement('div'); - el.innerHTML = ` - <div class="flash-container"> - <div class="flash"> - </div> - </div> - `; - }); - - it('does not throw', () => { - expect(() => addDismissFlashClickListener(el, false)).not.toThrow(); - }); - }); - }); }); diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/database_spec.rb index ed5b34b7f8c..bb7f8c63ee5 100644 --- a/spec/lib/backup/database_spec.rb +++ b/spec/lib/backup/database_spec.rb @@ -2,11 +2,14 @@ require 'spec_helper' -RSpec.describe Backup::Database do +RSpec.describe Backup::Database, feature_category: :backup_restore do let(:progress) { StringIO.new } let(:output) { progress.string } + let(:one_db_configured?) { Gitlab::Database.database_base_models.one? } + let(:database_models_for_backup) { Gitlab::Database.database_base_models_with_gitlab_shared } before(:all) do + Rake::Task.define_task(:environment) Rake.application.rake_require 'active_record/railties/databases' Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/shell' @@ -14,14 +17,106 @@ RSpec.describe Backup::Database do Rake.application.rake_require 'tasks/cache' end + describe '#dump', :delete do + let(:backup_id) { 'some_id' } + let(:force) { true } + + subject { described_class.new(progress, force: force) } + + before do + database_models_for_backup.each do |database_name, base_model| + base_model.connection.rollback_transaction unless base_model.connection.open_transactions.zero? + end + end + + it 'creates gzipped database dumps' do + Dir.mktmpdir do |dir| + subject.dump(dir, backup_id) + + database_models_for_backup.each_key do |database_name| + filename = database_name == 'main' ? 'database.sql.gz' : "#{database_name}_database.sql.gz" + expect(File.exist?(File.join(dir, filename))).to eq(true) + end + end + end + + it 'uses snapshots' do + Dir.mktmpdir do |dir| + base_model = Gitlab::Database.database_base_models['main'] + expect(base_model.connection).to receive(:begin_transaction).with( + isolation: :repeatable_read + ).and_call_original + expect(base_model.connection).to receive(:execute).with( + "SELECT pg_export_snapshot() as snapshot_id;" + ).and_call_original + expect(base_model.connection).to receive(:rollback_transaction).and_call_original + + subject.dump(dir, backup_id) + end + end + + describe 'pg_dump arguments' do + let(:snapshot_id) { 'fake_id' } + let(:pg_args) do + [ + '--clean', + '--if-exists', + "--snapshot=#{snapshot_id}" + ] + end + + let(:dumper) { double } + let(:destination_dir) { 'tmp' } + + before do + allow(Backup::Dump::Postgres).to receive(:new).and_return(dumper) + allow(dumper).to receive(:dump).with(any_args).and_return(true) + + database_models_for_backup.each do |database_name, base_model| + allow(base_model.connection).to receive(:execute).with( + "SELECT pg_export_snapshot() as snapshot_id;" + ).and_return(['snapshot_id' => snapshot_id]) + end + end + + it 'calls Backup::Dump::Postgres with correct pg_dump arguments' do + expect(dumper).to receive(:dump).with(anything, anything, pg_args) + + subject.dump(destination_dir, backup_id) + end + + context 'when a PostgreSQL schema is used' do + let(:schema) { 'gitlab' } + let(:additional_args) do + pg_args + ['-n', schema] + Gitlab::Database::EXTRA_SCHEMAS.flat_map do |schema| + ['-n', schema.to_s] + end + end + + before do + allow(Gitlab.config.backup).to receive(:pg_schema).and_return(schema) + end + + it 'calls Backup::Dump::Postgres with correct pg_dump arguments' do + expect(dumper).to receive(:dump).with(anything, anything, additional_args) + + subject.dump(destination_dir, backup_id) + end + end + end + end + describe '#restore' do let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1)] } - let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s } + let(:backup_dir) { Rails.root.join("spec/fixtures/") } let(:force) { true } + let(:rake_task) { instance_double(Rake::Task, invoke: true) } - subject { described_class.new(Gitlab::Database::MAIN_DATABASE_NAME.to_sym, progress, force: force) } + subject { described_class.new(progress, force: force) } before do + allow(Rake::Task).to receive(:[]).with(any_args).and_return(rake_task) + allow(subject).to receive(:pg_restore_cmd).and_return(cmd) end @@ -30,9 +125,14 @@ RSpec.describe Backup::Database do it 'warns the user and waits' do expect(subject).to receive(:sleep) - expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) - subject.restore(data) + if one_db_configured? + expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + else + expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) + end + + subject.restore(backup_dir) expect(output).to include('Removing all tables. Press `Ctrl-C` within 5 seconds to abort') end @@ -43,12 +143,14 @@ RSpec.describe Backup::Database do end context 'with an empty .gz file' do - let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s } - it 'returns successfully' do - expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + if one_db_configured? + expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + else + expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) + end - subject.restore(data) + subject.restore(backup_dir) expect(output).to include("Restoring PostgreSQL database") expect(output).to include("[DONE]") @@ -57,12 +159,18 @@ RSpec.describe Backup::Database do end context 'with a corrupted .gz file' do - let(:data) { Rails.root.join("spec/fixtures/big-image.png").to_s } + before do + allow(subject).to receive(:file_name).and_return("#{backup_dir}big-image.png") + end it 'raises a backup error' do - expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + if one_db_configured? + expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + else + expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) + end - expect { subject.restore(data) }.to raise_error(Backup::Error) + expect { subject.restore(backup_dir) }.to raise_error(Backup::Error) end end @@ -72,9 +180,13 @@ RSpec.describe Backup::Database do let(:cmd) { %W[#{Gem.ruby} -e $stderr.write("#{noise}#{visible_error}")] } it 'filters out noise from errors and has a post restore warning' do - expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + if one_db_configured? + expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + else + expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) + end - subject.restore(data) + subject.restore(backup_dir) expect(output).to include("ERRORS") expect(output).not_to include(noise) @@ -95,9 +207,13 @@ RSpec.describe Backup::Database do end it 'overrides default config values' do - expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + if one_db_configured? + expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) + else + expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) + end - subject.restore(data) + subject.restore(backup_dir) expect(output).to include(%("PGHOST"=>"test.example.com")) expect(output).to include(%("PGPASSWORD"=>"donotchange")) @@ -107,22 +223,30 @@ RSpec.describe Backup::Database do end context 'when the source file is missing' do - let(:main_database) { described_class.new(Gitlab::Database::MAIN_DATABASE_NAME.to_sym, progress, force: force) } - let(:ci_database) { described_class.new(Gitlab::Database::CI_DATABASE_NAME.to_sym, progress, force: force) } - let(:missing_file) { Rails.root.join("spec/fixtures/missing_file.tar.gz").to_s } - - it 'main database raises an error about missing source file' do - expect(Rake::Task['gitlab:db:drop_tables']).not_to receive(:invoke) - - expect do - main_database.restore(missing_file) - end.to raise_error(Backup::Error, /Source database file does not exist/) + context 'for main database' do + before do + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("#{backup_dir}database.sql.gz").and_return(false) + allow(File).to receive(:exist?).with("#{backup_dir}ci_database.sql.gz").and_return(false) + end + + it 'raises an error about missing source file' do + if one_db_configured? + expect(Rake::Task['gitlab:db:drop_tables']).not_to receive(:invoke) + else + expect(Rake::Task['gitlab:db:drop_tables:main']).not_to receive(:invoke) + end + + expect do + subject.restore('db') + end.to raise_error(Backup::Error, /Source database file does not exist/) + end end - it 'ci database tolerates missing source file' do - expect(Rake::Task['gitlab:db:drop_tables']).not_to receive(:invoke) - skip_if_multiple_databases_not_setup - expect { ci_database.restore(missing_file) }.not_to raise_error + context 'for ci database' do + it 'ci database tolerates missing source file' do + expect { subject.restore(backup_dir) }.not_to raise_error + end end end end diff --git a/spec/lib/backup/dump/postgres_spec.rb b/spec/lib/backup/dump/postgres_spec.rb new file mode 100644 index 00000000000..f6a68ab6db9 --- /dev/null +++ b/spec/lib/backup/dump/postgres_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Backup::Dump::Postgres, feature_category: :backup_restore do + describe '#dump' do + let(:pg_database) { 'gitlabhq_test' } + let(:destination_dir) { Dir.mktmpdir } + let(:db_file_name) { File.join(destination_dir, 'output.gz') } + + let(:pipes) { IO.pipe } + let(:gzip_pid) { spawn('gzip -c -1', in: pipes[0], out: [db_file_name, 'w', 0o600]) } + let(:pg_dump_pid) { Process.spawn('pg_dump', *args, pg_database, out: pipes[1]) } + let(:args) { ['--help'] } + + subject { described_class.new } + + before do + allow(IO).to receive(:pipe).and_return(pipes) + end + + after do + FileUtils.remove_entry destination_dir + end + + it 'creates gzipped dump using supplied arguments' do + expect(subject).to receive(:spawn).with('gzip -c -1', in: pipes.first, + out: [db_file_name, 'w', 0o600]).and_return(gzip_pid) + expect(Process).to receive(:spawn).with('pg_dump', *args, pg_database, out: pipes[1]).and_return(pg_dump_pid) + + subject.dump(pg_database, db_file_name, args) + + expect(File.exist?(db_file_name)).to eq(true) + end + end +end diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 992dbec73c2..02889c1535d 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Backup::Manager do +RSpec.describe Backup::Manager, feature_category: :backup_restore do include StubENV let(:progress) { StringIO.new } @@ -30,8 +30,7 @@ RSpec.describe Backup::Manager do task: task, enabled: enabled, destination_path: 'my_task.tar.gz', - human_name: 'my task', - task_group: 'group1' + human_name: 'my task' ) } end @@ -63,16 +62,6 @@ RSpec.describe Backup::Manager do subject.run_create_task('my_task') end end - - describe 'task group skipped' do - it 'informs the user' do - stub_env('SKIP', 'group1') - - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping my task ... [SKIPPED]') - - subject.run_create_task('my_task') - end - end end describe '#run_restore_task' do diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb index d2f32b60464..d5a07f200dd 100644 --- a/spec/support/matchers/schema_matcher.rb +++ b/spec/support/matchers/schema_matcher.rb @@ -16,20 +16,8 @@ module SchemaPath end def self.validator(schema_path) - unless @schema_cache.key?(schema_path) - @schema_cache[schema_path] = JSONSchemer.schema(schema_path, ref_resolver: SchemaPath.file_ref_resolver) - end - - @schema_cache[schema_path] - end - - def self.file_ref_resolver - proc do |uri| - file = Rails.root.join(uri.path) - raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json') - raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist? - - Gitlab::Json.parse(File.read(file)) + @schema_cache.fetch(schema_path) do + @schema_cache[schema_path] = JSONSchemer.schema(schema_path) end end end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index 4df12f7849b..bdb01b12607 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -12,27 +12,20 @@ RSpec.shared_examples 'misconfigured dashboard service response' do |status_code end RSpec.shared_examples 'valid dashboard service response for schema' do - file_ref_resolver = proc do |uri| - file = Rails.root.join(uri.path) - raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json') - raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist? - - Gitlab::Json.parse(File.read(file)) - end - it 'returns a json representation of the dashboard' do result = service_call expect(result.keys).to contain_exactly(:dashboard, :status) expect(result[:status]).to eq(:success) - validator = JSONSchemer.schema(dashboard_schema, ref_resolver: file_ref_resolver) + schema_path = Rails.root.join('spec/fixtures', dashboard_schema) + validator = JSONSchemer.schema(schema_path) expect(validator.valid?(result[:dashboard].with_indifferent_access)).to be true end end RSpec.shared_examples 'valid dashboard service response' do - let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) } + let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/dashboard.json' } it_behaves_like 'valid dashboard service response for schema' end @@ -76,7 +69,7 @@ RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file end RSpec.shared_examples 'valid embedded dashboard service response' do - let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) } + let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json' } it_behaves_like 'valid dashboard service response for schema' end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 972851cba8c..4aa6edf4789 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -7,9 +7,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete, feature_category: :bac let(:backup_restore_pid_path) { "#{Rails.application.root}/tmp/backup_restore.pid" } let(:backup_tasks) { %w[db repo uploads builds artifacts pages lfs terraform_state registry packages] } let(:backup_types) do - %w[main_db repositories uploads builds artifacts pages lfs terraform_state registry packages].tap do |array| - array.insert(1, 'ci_db') if Gitlab::Database.has_config?(:ci) - end + %w[db repositories uploads builds artifacts pages lfs terraform_state registry packages] end def tars_glob @@ -94,7 +92,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete, feature_category: :bac let(:pid_file) { instance_double(File, write: 12345) } where(:tasks_name, :rake_task) do - %w[main_db ci_db] | 'gitlab:backup:db:restore' + 'db' | 'gitlab:backup:db:restore' 'repositories' | 'gitlab:backup:repo:restore' 'builds' | 'gitlab:backup:builds:restore' 'uploads' | 'gitlab:backup:uploads:restore' @@ -260,9 +258,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete, feature_category: :bac end it 'logs the progress to log file' do - ci_database_status = Gitlab::Database.has_config?(:ci) ? "[SKIPPED]" : "[DISABLED]" - expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping main_database ... [SKIPPED]") - expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping ci_database ... #{ci_database_status}") + expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping database ... [SKIPPED]") expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping repositories ... ") expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping repositories ... done") expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping uploads ... ") diff --git a/workhorse/go.mod b/workhorse/go.mod index 03728719cac..70439ee2bae 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -32,10 +32,10 @@ require ( gocloud.dev v0.28.0 golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 - golang.org/x/net v0.4.0 + golang.org/x/net v0.5.0 golang.org/x/oauth2 v0.2.0 golang.org/x/tools v0.2.0 - google.golang.org/grpc v1.51.0 + google.golang.org/grpc v1.52.0 google.golang.org/protobuf v1.28.1 honnef.co/go/tools v0.3.3 ) @@ -44,7 +44,7 @@ require ( cloud.google.com/go v0.107.0 // indirect cloud.google.com/go/compute v1.13.0 // indirect cloud.google.com/go/compute/metadata v0.2.2 // indirect - cloud.google.com/go/iam v0.7.0 // indirect + cloud.google.com/go/iam v0.8.0 // indirect cloud.google.com/go/monitoring v1.9.0 // indirect cloud.google.com/go/profiler v0.1.0 // indirect cloud.google.com/go/storage v1.28.0 // indirect @@ -111,13 +111,13 @@ require ( golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect golang.org/x/mod v0.6.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect golang.org/x/time v0.2.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.103.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3 // indirect + google.golang.org/genproto v0.0.0-20230117162540-28d6b9783ac4 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/workhorse/go.sum b/workhorse/go.sum index 0eafc108726..dbb22f7a90a 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -203,8 +203,9 @@ cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSu cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= @@ -2118,8 +2119,9 @@ golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2320,8 +2322,9 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2341,8 +2344,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2646,8 +2650,9 @@ google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+ google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3 h1:BCcW+lhENGqZ2R2MsM9oty220E8vY9E4QC1Tq05hN1E= google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20230117162540-28d6b9783ac4 h1:yF0uHwqqYt2tIL2F4hxRWA1ZFX43SEunWAK8MnQiclk= +google.golang.org/genproto v0.0.0-20230117162540-28d6b9783ac4/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2693,8 +2698,9 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |