Commit 8ba98c1a authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into 3551-epic-issues

* master: (22 commits)
  Delete epic
  Fix markdown for other components
  Fixup markdown document structure
  Correct typo: bundle -> bundled
  Prune setting of postgresql['sql_user_password']
  [Geo][error handling] [address review comments] catching some errors
  Move EE-only /app/workers files under top-level /ee directory
  Fix approvals responsive wrapping on mobile
  EE port of enable-scss-lint-unnecessary-mantissa
  Update docs for ajusted queries
  Update queries to remove duplicate entries
  Geo: address review comments
  Geo: Fix static analysis
  update changelog
  GEO: Error handling - Specs
  GEO: Address review comments
  Geo: Implement a download to a temporary repo
  Geo: Add specs for error handling
  Geo: Add progressive backoff for file download retries[ci skip]
  Geo Add forced redownload for some type of errors[ci skip]
  ...
parents d284f678 e9b7ea60
...@@ -241,7 +241,7 @@ linters: ...@@ -241,7 +241,7 @@ linters:
# Numeric values should not contain unnecessary fractional portions. # Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa: UnnecessaryMantissa:
enabled: false enabled: true
# Do not use parent selector references (&) when they would otherwise # Do not use parent selector references (&) when they would otherwise
# be unnecessary. # be unnecessary.
......
...@@ -29,6 +29,11 @@ export default { ...@@ -29,6 +29,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
...@@ -92,6 +97,11 @@ export default { ...@@ -92,6 +97,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
issuableType: {
type: String,
required: false,
default: 'issue',
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
...@@ -157,21 +167,21 @@ export default { ...@@ -157,21 +167,21 @@ export default {
}) })
.catch(() => { .catch(() => {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
window.Flash('Error updating issue'); window.Flash(`Error updating ${this.issuableType}`);
}); });
}, },
deleteIssuable() { deleteIssuable() {
this.service.deleteIssuable() this.service.deleteIssuable()
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
// Stop the poll so we don't get 404's with the issue not existing // Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop(); this.poll.stop();
gl.utils.visitUrl(data.web_url); gl.utils.visitUrl(data.web_url);
}) })
.catch(() => { .catch(() => {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
window.Flash('Error deleting issue'); window.Flash(`Error deleting ${this.issuableType}`);
}); });
}, },
}, },
...@@ -223,6 +233,7 @@ export default { ...@@ -223,6 +233,7 @@ export default {
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
/> />
<div v-else> <div v-else>
<title-component <title-component
......
...@@ -13,6 +13,11 @@ ...@@ -13,6 +13,11 @@
type: Object, type: Object,
required: true, required: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -23,6 +28,9 @@ ...@@ -23,6 +28,9 @@
isSubmitEnabled() { isSubmitEnabled() {
return this.formState.title.trim() !== ''; return this.formState.title.trim() !== '';
}, },
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
}, },
methods: { methods: {
closeForm() { closeForm() {
...@@ -62,7 +70,7 @@ ...@@ -62,7 +70,7 @@
Cancel Cancel
</button> </button>
<button <button
v-if="canDestroy" v-if="shouldShowDeleteButton"
class="btn btn-danger pull-right append-right-default" class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }" :class="{ disabled: deleteLoading }"
type="button" type="button"
......
...@@ -36,6 +36,11 @@ ...@@ -36,6 +36,11 @@
type: String, type: String,
required: true, required: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
}, },
components: { components: {
lockedWarning, lockedWarning,
...@@ -81,6 +86,7 @@ ...@@ -81,6 +86,7 @@
:markdown-docs-path="markdownDocsPath" /> :markdown-docs-path="markdownDocsPath" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" /> :can-destroy="canDestroy"
:show-delete-button="showDeleteButton" />
</form> </form>
</template> </template>
...@@ -35,6 +35,11 @@ export default { ...@@ -35,6 +35,11 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
containerClass: {
type: String,
required: false,
default: 'btn btn-align-content',
},
}, },
components: { components: {
loadingIcon, loadingIcon,
...@@ -49,9 +54,9 @@ export default { ...@@ -49,9 +54,9 @@ export default {
<template> <template>
<button <button
class="btn btn-align-content"
@click="onClick" @click="onClick"
type="button" type="button"
:class="containerClass"
:disabled="loading || disabled" :disabled="loading || disabled"
> >
<transition name="fade"> <transition name="fade">
......
...@@ -353,3 +353,7 @@ ...@@ -353,3 +353,7 @@
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
} }
.flex-right {
margin-left: auto;
}
...@@ -101,13 +101,13 @@ ...@@ -101,13 +101,13 @@
@for $i from 0 through 5 { @for $i from 0 through 5 {
.legend-box-#{$i} { .legend-box-#{$i} {
background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
} }
} }
@for $i from 1 through 4 { @for $i from 1 through 4 {
.legend-box-#{$i + 5} { .legend-box-#{$i + 5} {
background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
} }
} }
} }
...@@ -200,13 +200,13 @@ ...@@ -200,13 +200,13 @@
@for $i from 0 through 5 { @for $i from 0 through 5 {
td.blame-commit-age-#{$i} { td.blame-commit-age-#{$i} {
border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
} }
} }
@for $i from 1 through 4 { @for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} { td.blame-commit-age-#{$i + 5} {
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
} }
} }
} }
......
...@@ -164,7 +164,7 @@ $gl-text-color: #2e2e2e; ...@@ -164,7 +164,7 @@ $gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070; $gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494; $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6; $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0); $gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600; $gl-text-green: $green-600;
$gl-text-green-hover: $green-700; $gl-text-green-hover: $green-700;
...@@ -493,8 +493,8 @@ $callout-success-color: $green-700; ...@@ -493,8 +493,8 @@ $callout-success-color: $green-700;
/* /*
* Commit Page * Commit Page
*/ */
$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); $commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); $commit-message-text-area-bg: rgba(0, 0, 0, 0);
/* /*
* Common * Common
......
...@@ -333,7 +333,7 @@ ...@@ -333,7 +333,7 @@
.prometheus-graph-overlay { .prometheus-graph-overlay {
fill: none; fill: none;
opacity: 0.0; opacity: 0;
pointer-events: all; pointer-events: all;
} }
......
...@@ -715,13 +715,16 @@ ...@@ -715,13 +715,16 @@
.approvals-footer { .approvals-footer {
display: flex; display: flex;
.approvers-prefix, .approvers-prefix {
.approvers-list {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.approvers-list { .approvers-list {
display: flex;
align-items: center;
.link-to-member-avatar:not(:first-child) { .link-to-member-avatar:not(:first-child) {
img { img {
margin-left: 0; margin-left: 0;
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.diff-file .diff-content { .diff-file .diff-content {
tr.line_holder:hover > td .line_note_link { tr.line_holder:hover > td .line_note_link {
opacity: 1.0; opacity: 1;
filter: alpha(opacity = 100); filter: alpha(opacity = 100);
} }
} }
......
class Geo::FileRegistry < Geo::BaseRegistry class Geo::FileRegistry < Geo::BaseRegistry
scope :failed, -> { where(success: false) } scope :failed, -> { where(success: false) }
scope :synced, -> { where(success: true) } scope :synced, -> { where(success: true) }
scope :retry_due, -> { where('retry_at is NULL OR retry_at < ?', Time.now) }
scope :lfs_objects, -> { where(file_type: :lfs) } scope :lfs_objects, -> { where(file_type: :lfs) }
scope :attachments, -> { where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES) } scope :attachments, -> { where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES) }
end end
...@@ -15,6 +15,15 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -15,6 +15,15 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
where(repository_sync_failed.or(wiki_sync_failed)) where(repository_sync_failed.or(wiki_sync_failed))
end end
def self.retry_due
where(
arel_table[:repository_retry_at].lt(Time.now)
.or(arel_table[:wiki_retry_at].lt(Time.now))
.or(arel_table[:repository_retry_at].eq(nil))
.or(arel_table[:wiki_retry_at].eq(nil))
)
end
def self.synced def self.synced
where.not(last_repository_synced_at: nil, last_repository_successful_sync_at: nil) where.not(last_repository_synced_at: nil, last_repository_successful_sync_at: nil)
.where(resync_repository: false, resync_wiki: false) .where(resync_repository: false, resync_wiki: false)
...@@ -39,10 +48,16 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -39,10 +48,16 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
end end
def repository_sync_needed?(timestamp) def repository_sync_needed?(timestamp)
resync_repository? && (last_repository_synced_at.nil? || timestamp > last_repository_synced_at) return false unless resync_repository?
return false if repository_retry_at && timestamp < repository_retry_at
last_repository_synced_at.nil? || timestamp > last_repository_synced_at
end end
def wiki_sync_needed?(timestamp) def wiki_sync_needed?(timestamp)
resync_wiki? && (last_wiki_synced_at.nil? || timestamp > last_wiki_synced_at) return false unless resync_wiki?
return false if wiki_retry_at && timestamp < wiki_retry_at
last_wiki_synced_at.nil? || timestamp > last_wiki_synced_at
end end
end end
require 'securerandom'
module Geo module Geo
# The clone_url_prefix is used to build URLs for the Geo synchronization # The clone_url_prefix is used to build URLs for the Geo synchronization
# If this is missing from the primary node we raise this exception # If this is missing from the primary node we raise this exception
...@@ -6,6 +8,7 @@ module Geo ...@@ -6,6 +8,7 @@ module Geo
class BaseSyncService class BaseSyncService
include ExclusiveLeaseGuard include ExclusiveLeaseGuard
include ::Gitlab::Geo::ProjectLogHelpers include ::Gitlab::Geo::ProjectLogHelpers
include Delay
class << self class << self
attr_accessor :type attr_accessor :type
...@@ -15,6 +18,8 @@ module Geo ...@@ -15,6 +18,8 @@ module Geo
LEASE_TIMEOUT = 8.hours.freeze LEASE_TIMEOUT = 8.hours.freeze
LEASE_KEY_PREFIX = 'geo_sync_service'.freeze LEASE_KEY_PREFIX = 'geo_sync_service'.freeze
RETRY_BEFORE_REDOWNLOAD = 5
RETRY_LIMIT = 8
def initialize(project) def initialize(project)
@project = project @project = project
...@@ -23,7 +28,18 @@ module Geo ...@@ -23,7 +28,18 @@ module Geo
def execute def execute
try_obtain_lease do try_obtain_lease do
log_info("Started #{type} sync") log_info("Started #{type} sync")
sync_repository
if should_be_retried?
sync_repository
elsif should_be_redownloaded?
sync_repository(true)
else
# Clean up the state of sync to start a new cycle
registry.delete
log_info("Clean up #{type} sync status")
return
end
log_info("Finished #{type} sync") log_info("Finished #{type} sync")
end end
end end
...@@ -48,6 +64,22 @@ module Geo ...@@ -48,6 +64,22 @@ module Geo
private private
def retry_count
registry.public_send("#{type}_retry_count") || 0 # rubocop:disable GitlabSecurity/PublicSend
end
def should_be_retried?
return false if registry.public_send("force_to_redownload_#{type}") # rubocop:disable GitlabSecurity/PublicSend
retry_count <= RETRY_BEFORE_REDOWNLOAD
end
def should_be_redownloaded?
return true if registry.public_send("force_to_redownload_#{type}") # rubocop:disable GitlabSecurity/PublicSend
(RETRY_BEFORE_REDOWNLOAD..RETRY_LIMIT) === retry_count
end
def sync_repository def sync_repository
raise NotImplementedError, 'This class should implement sync_repository method' raise NotImplementedError, 'This class should implement sync_repository method'
end end
...@@ -101,11 +133,17 @@ module Geo ...@@ -101,11 +133,17 @@ module Geo
attrs = {} attrs = {}
attrs["last_#{type}_synced_at"] = started_at if started_at if started_at
attrs["last_#{type}_synced_at"] = started_at
attrs["#{type}_retry_count"] = retry_count + 1
attrs["#{type}_retry_at"] = Time.now + delay(attrs["#{type}_retry_count"]).seconds
end
if finished_at if finished_at
attrs["last_#{type}_successful_sync_at"] = finished_at attrs["last_#{type}_successful_sync_at"] = finished_at
attrs["resync_#{type}"] = false attrs["resync_#{type}"] = false
attrs["#{type}_retry_count"] = nil
attrs["#{type}_retry_at"] = nil
end end
registry.update!(attrs) registry.update!(attrs)
...@@ -134,5 +172,43 @@ module Geo ...@@ -134,5 +172,43 @@ module Geo
def last_synced_at def last_synced_at
registry.public_send("last_#{type}_synced_at") # rubocop:disable GitlabSecurity/PublicSend registry.public_send("last_#{type}_synced_at") # rubocop:disable GitlabSecurity/PublicSend
end end
def disk_path_temp
unless @disk_path_temp
random_string = SecureRandom.hex(7)
@disk_path_temp = "#{repository.disk_path}_#{random_string}"
end
@disk_path_temp
end
def build_temporary_repository
unless gitlab_shell.add_repository(project.repository_storage, disk_path_temp)
raise Gitlab::Shell::Error, 'Can not create a temporary repository'
end
repository.clone.tap { |repo| repo.disk_path = disk_path_temp }
end
def clean_up_temporary_repository
gitlab_shell.remove_repository(project.repository_storage_path, disk_path_temp)
end
def set_temp_repository_as_main
log_info(
"Setting newly downloaded repository as main",
storage_path: project.repository_storage_path,
temp_path: disk_path_temp,
disk_path: repository.disk_path
)
unless gitlab_shell.remove_repository(project.repository_storage_path, repository.disk_path)
raise Gitlab::Shell::Error, 'Can not remove outdated main repository to replace it'
end
unless gitlab_shell.mv_repository(project.repository_storage_path, disk_path_temp, repository.disk_path)
raise Gitlab::Shell::Error, 'Can not move temporary repository'
end
end
end end
end end
...@@ -2,6 +2,8 @@ module Geo ...@@ -2,6 +2,8 @@ module Geo
class FileDownloadService < FileService class FileDownloadService < FileService
LEASE_TIMEOUT = 8.hours.freeze LEASE_TIMEOUT = 8.hours.freeze
include Delay
def execute def execute
try_obtain_lease do |lease| try_obtain_lease do |lease|
start_time = Time.now start_time = Time.now
...@@ -45,6 +47,13 @@ module Geo ...@@ -45,6 +47,13 @@ module Geo
transfer.bytes = bytes_downloaded transfer.bytes = bytes_downloaded
transfer.success = success transfer.success = success
unless success
# We don't limit the amount of retries
transfer.retry_count = (transfer.retry_count || 0) + 1
transfer.retry_at = Time.now + delay(transfer.retry_count).seconds
end
transfer.save transfer.save
end end
......
module Geo module Geo
class RepositorySyncService < BaseSyncService class RepositorySyncService < BaseSyncService
include Gitlab::ShellAdapter
self.type = :repository self.type = :repository
private private
def sync_repository def sync_repository(redownload = false)
fetch_project_repository fetch_project_repository(redownload)
expire_repository_caches expire_repository_caches
end end
def fetch_project_repository def fetch_project_repository(redownload)
log_info('Fetching project repository') log_info('Trying to fetch project repository')
update_registry(started_at: DateTime.now) update_registry(started_at: DateTime.now)
begin if redownload
log_info('Redownloading repository')
fetch_geo_mirror(build_temporary_repository)
set_temp_repository_as_main
else
project.ensure_repository project.ensure_repository
fetch_geo_mirror(project.repository) fetch_geo_mirror(project.repository)
update_registry(finished_at: DateTime.now)
log_info("Finished repository sync",
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
rescue Gitlab::Shell::Error,
Gitlab::Git::RepositoryMirroring::RemoteError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing repository', e)
rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid repository', e)
log_info('Expiring caches')
project.repository.after_create
end end
update_registry(finished_at: DateTime.now)
log_info('Finished repository sync',
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
rescue Gitlab::Shell::Error,
Gitlab::Git::RepositoryMirroring::RemoteError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing repository', e)
rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid repository', e)
log_info('Setting force_to_redownload flag')
registry.update(force_to_redownload_repository: true)
log_info('Expiring caches')
project.repository.after_create
ensure
clean_up_temporary_repository if redownload
end end
def expire_repository_caches def expire_repository_caches
...@@ -40,5 +50,9 @@ module Geo ...@@ -40,5 +50,9 @@ module Geo
def ssh_url_to_repo def ssh_url_to_repo
"#{primary_ssh_path_prefix}#{project.full_path}.git" "#{primary_ssh_path_prefix}#{project.full_path}.git"
end end
def repository
project.repository
end
end end
end end
module Geo module Geo
class WikiSyncService < BaseSyncService class WikiSyncService < BaseSyncService
include Gitlab::ShellAdapter
self.type = :wiki self.type = :wiki
private private
def sync_repository def sync_repository(redownload = false)
fetch_wiki_repository fetch_wiki_repository(redownload)
end end
def fetch_wiki_repository def fetch_wiki_repository(redownload)
log_info('Fetching wiki repository') log_info('Fetching wiki repository')
update_registry(started_at: DateTime.now) update_registry(started_at: DateTime.now)
begin if redownload
log_info('Redownloading wiki')
fetch_geo_mirror(build_temporary_repository)
set_temp_repository_as_main
else
project.wiki.ensure_repository project.wiki.ensure_repository
fetch_geo_mirror(project.wiki.repository) fetch_geo_mirror(project.wiki.repository)
update_registry(finished_at: DateTime.now)
log_info("Finished wiki sync",
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
rescue Gitlab::Git::Repository::NoRepository,
Gitlab::Git::RepositoryMirroring::RemoteError,
Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing wiki repository', e)
end end
update_registry(finished_at: DateTime.now)
log_info('Finished wiki sync',
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
rescue Gitlab::Git::RepositoryMirroring::RemoteError,
Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing wiki repository', e)
rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid wiki', e)
registry.update(force_to_redownload_wiki: true)
ensure
clean_up_temporary_repository if redownload
end end
def ssh_url_to_wiki def ssh_url_to_wiki
"#{primary_ssh_path_prefix}#{project.full_path}.wiki.git" "#{primary_ssh_path_prefix}#{project.full_path}.wiki.git"
end end
def repository
project.wiki.repository
end
end end
end end
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# when the user is destroyed. # when the user is destroyed.
module Users module Users
class MigrateToGhostUserService class MigrateToGhostUserService
prepend EE::Users::MigrateToGhostUserService
extend ActiveSupport::Concern extend ActiveSupport::Concern
attr_reader :ghost_user, :user attr_reader :ghost_user, :user
......
module Delay
# Progressive backoff. It's copied from Sidekiq as is
def delay(retry_count = 0)
(retry_count**4) + 15 + (rand(30) * (retry_count + 1))
end
end
...@@ -33,6 +33,7 @@ module Geo ...@@ -33,6 +33,7 @@ module Geo
def find_failed_objects(batch_size:) def find_failed_objects(batch_size:)
Geo::FileRegistry Geo::FileRegistry
.failed .failed
.retry_due
.limit(batch_size) .limit(batch_size)
.pluck(:file_id, :file_type) .pluck(:file_id, :file_type)
end end
......
...@@ -33,6 +33,7 @@ module Geo ...@@ -33,6 +33,7 @@ module Geo
def find_project_ids_updated_recently(batch_size:) def find_project_ids_updated_recently(batch_size:)
current_node.project_registries current_node.project_registries
.dirty .dirty
.retry_due
.order(Gitlab::Database.nulls_first_order(:last_repository_synced_at, :desc)) .order(Gitlab::Database.nulls_first_order(:last_repository_synced_at, :desc))
.limit(batch_size) .limit(batch_size)
.pluck(:project_id) .pluck(:project_id)
......
---
title: Improve error handling
merge_request:
author:
type: added
---
title: Fix Merge Request Widget Approvals responsiveness on mobile
merge_request:
author:
type: fixed
---
title: Add delete epic button
merge_request:
author:
type: added
...@@ -145,10 +145,10 @@ ...@@ -145,10 +145,10 @@
- container_memory_usage_bytes - container_memory_usage_bytes
weight: 1 weight: 1
queries: queries:
- query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"})) /1024/1024' - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
- query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"})) /1024/1024' - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
track: canary track: canary
...@@ -158,10 +158,10 @@ ...@@ -158,10 +158,10 @@
- container_cpu_usage_seconds_total - container_cpu_usage_seconds_total
weight: 1 weight: 1
queries: queries:
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100' - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
label: CPU label: Average
unit: "%" unit: "%"
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) * 100' - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) without (job)) * 100'
label: CPU label: Average
unit: "%" unit: "%"
track: canary track: canary
\ No newline at end of file
class AddRetryCountFieldsToRegistries < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :file_registry, :retry_count, :integer
add_column :file_registry, :retry_at, :datetime
add_column :project_registry, :repository_retry_count, :integer
add_column :project_registry, :repository_retry_at, :datetime
add_column :project_registry, :force_to_redownload_repository, :boolean
add_column :project_registry, :wiki_retry_count, :integer
add_column :project_registry, :wiki_retry_at, :datetime
add_column :project_registry, :force_to_redownload_wiki, :boolean
# Indecies
add_concurrent_index :file_registry, :retry_at
add_concurrent_index :project_registry, :repository_retry_at
add_concurrent_index :project_registry, :wiki_retry_at
end
def down
remove_column :file_registry, :retry_count, :integer
remove_column :file_registry, :retry_at, :datetime
remove_column :project_registry, :repository_retry_count, :integer
remove_column :project_registry, :repository_retry_at, :datetime
remove_column :project_registry, :force_to_redownload_repository, :boolean
remove_column :project_registry, :wiki_retry_count, :integer
remove_column :project_registry, :wiki_retry_at, :datetime
remove_column :project_registry, :force_to_redownload_wiki, :boolean
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171009162209) do ActiveRecord::Schema.define(version: 20171101105200) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -26,10 +26,13 @@ ActiveRecord::Schema.define(version: 20171009162209) do ...@@ -26,10 +26,13 @@ ActiveRecord::Schema.define(version: 20171009162209) do
t.string "sha256" t.string "sha256"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.boolean "success", default: false, null: false t.boolean "success", default: false, null: false
t.integer "retry_count"
t.datetime "retry_at"
end end
add_index "file_registry", ["file_type", "file_id"], name: "index_file_registry_on_file_type_and_file_id", unique: true, using: :btree add_index "file_registry", ["file_type", "file_id"], name: "index_file_registry_on_file_type_and_file_id", unique: true, using: :btree
add_index "file_registry", ["file_type"], name: "index_file_registry_on_file_type", using: :btree add_index "file_registry", ["file_type"], name: "index_file_registry_on_file_type", using: :btree
add_index "file_registry", ["retry_at"], name: "index_file_registry_on_retry_at", using: :btree
add_index "file_registry", ["success"], name: "index_file_registry_on_success", using: :btree add_index "file_registry", ["success"], name: "index_file_registry_on_success", using: :btree
create_table "project_registry", force: :cascade do |t| create_table "project_registry", force: :cascade do |t|
...@@ -41,12 +44,20 @@ ActiveRecord::Schema.define(version: 20171009162209) do ...@@ -41,12 +44,20 @@ ActiveRecord::Schema.define(version: 20171009162209) do
t.boolean "resync_wiki", default: true, null: false t.boolean "resync_wiki", default: true, null: false
t.datetime "last_wiki_synced_at" t.datetime "last_wiki_synced_at"
t.datetime "last_wiki_successful_sync_at" t.datetime "last_wiki_successful_sync_at"
t.integer "repository_retry_count"
t.datetime "repository_retry_at"
t.boolean "force_to_redownload_repository"
t.integer "wiki_retry_count"
t.datetime "wiki_retry_at"
t.boolean "force_to_redownload_wiki"
end end
add_index "project_registry", ["last_repository_successful_sync_at"], name: "index_project_registry_on_last_repository_successful_sync_at", using: :btree add_index "project_registry", ["last_repository_successful_sync_at"], name: "index_project_registry_on_last_repository_successful_sync_at", using: :btree
add_index "project_registry", ["last_repository_synced_at"], name: "index_project_registry_on_last_repository_synced_at", using: :btree add_index "project_registry", ["last_repository_synced_at"], name: "index_project_registry_on_last_repository_synced_at", using: :btree
add_index "project_registry", ["project_id"], name: "index_project_registry_on_project_id", unique: true, using: :btree add_index "project_registry", ["project_id"], name: "index_project_registry_on_project_id", unique: true, using: :btree
add_index "project_registry", ["repository_retry_at"], name: "index_project_registry_on_repository_retry_at", using: :btree
add_index "project_registry", ["resync_repository"], name: "index_project_registry_on_resync_repository", using: :btree add_index "project_registry", ["resync_repository"], name: "index_project_registry_on_resync_repository", using: :btree
add_index "project_registry", ["resync_wiki"], name: "index_project_registry_on_resync_wiki", using: :btree add_index "project_registry", ["resync_wiki"], name: "index_project_registry_on_resync_wiki", using: :btree
add_index "project_registry", ["wiki_retry_at"], name: "index_project_registry_on_wiki_retry_at", using: :btree
end end
...@@ -114,6 +114,7 @@ We will need the following password information for the application's database u ...@@ -114,6 +114,7 @@ We will need the following password information for the application's database u
- `POSTGRESQL_USER_PASSWORD`. The password for the database user - `POSTGRESQL_USER_PASSWORD`. The password for the database user
- `POSTGRESQL_PASSWORD_HASH`. The md5 hash of POSTGRESQL_USER_PASSWORD - `POSTGRESQL_PASSWORD_HASH`. The md5 hash of POSTGRESQL_USER_PASSWORD
#### Pgbouncer #### Pgbouncer
When using default setup, minimum configuration requires: When using default setup, minimum configuration requires:
...@@ -231,7 +232,6 @@ See `START user configuration` section in the next step for required information ...@@ -231,7 +232,6 @@ See `START user configuration` section in the next step for required information
postgresql['hot_standby'] = 'on' postgresql['hot_standby'] = 'on'
postgresql['wal_level'] = 'replica' postgresql['wal_level'] = 'replica'
postgresql['shared_preload_libraries'] = 'repmgr_funcs' postgresql['shared_preload_libraries'] = 'repmgr_funcs'
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
# Disable automatic database migrations # Disable automatic database migrations
gitlab_rails['auto_migrate'] = false gitlab_rails['auto_migrate'] = false
...@@ -626,9 +626,9 @@ On the consul server nodes, it is important to restart the consul service in a c ...@@ -626,9 +626,9 @@ On the consul server nodes, it is important to restart the consul service in a c
If you're running into an issue with a component not outlined here, be sure to check the troubleshooting section of their specific documentation page. If you're running into an issue with a component not outlined here, be sure to check the troubleshooting section of their specific documentation page.
[Consul](consul.md#troubleshooting) - [Consul](consul.md#troubleshooting)
[PostgreSQL](http://docs.gitlab.com/omnibus/settings/database.html#troubleshooting) - [PostgreSQL](http://docs.gitlab.com/omnibus/settings/database.html#troubleshooting)
[GitLab application](gitlab.md#troubleshooting) - [GitLab application](gitlab.md#troubleshooting)
--- ---
......
# Working with the bundle Consul service # Working with the bundled Consul service
## Overview ## Overview
......
...@@ -13,8 +13,8 @@ integration services must be enabled. ...@@ -13,8 +13,8 @@ integration services must be enabled.
| Name | Query | | Name | Query |
| ---- | ----- | | ---- | ----- |
| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 | | Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 |
| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 | | Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 |
## Configuring Prometheus to monitor for Kubernetes node metrics ## Configuring Prometheus to monitor for Kubernetes node metrics
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import { s__ } from '~/locale';
export default { export default {
name: 'epicHeader', name: 'epicHeader',
...@@ -15,6 +17,16 @@ ...@@ -15,6 +17,16 @@
type: String, type: String,
required: true, required: true,
}, },
canDelete: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
deleteLoading: false,
};
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -22,6 +34,15 @@ ...@@ -22,6 +34,15 @@
components: { components: {
userAvatarLink, userAvatarLink,
timeagoTooltip, timeagoTooltip,
loadingButton,
},
methods: {
deleteEpic() {
if (confirm(s__('Epic will be removed! Are you sure?'))) { // eslint-disable-line no-alert
this.deleteLoading = true;
this.$emit('deleteEpic');
}
},
}, },
}; };
</script> </script>
...@@ -29,21 +50,26 @@ ...@@ -29,21 +50,26 @@
<template> <template>
<div class="detail-page-header"> <div class="detail-page-header">
<div class="issuable-meta"> <div class="issuable-meta">
Opened {{ s__('Opened') }}
<timeagoTooltip <timeago-tooltip :time="created" />
:time="created" {{ s__('by') }}
/>
by
<strong> <strong>
<user-avatar-link <user-avatar-link
:link-href="author.url" :link-href="author.url"
:img-src="author.src" :img-src="author.src"
:img-size="24" :img-size="24"
:tooltipText="author.username" :tooltip-text="author.username"
:username="author.name" :username="author.name"
imgCssClasses="avatar-inline" img-css-classes="avatar-inline"
/> />
</strong> </strong>
</div> </div>
<loading-button
v-if="canDelete"
:loading="deleteLoading"
@click="deleteEpic"
:label="s__('Delete')"
container-class="btn btn-remove btn-inverted flex-right"
/>
</div> </div>
</template> </template>
<script> <script>
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
<<<<<<< HEAD
import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue'; import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue';
=======
import issuableAppEventHub from '~/issue_show/event_hub';
>>>>>>> master
import epicHeader from './epic_header.vue'; import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue'; import epicSidebar from '../../sidebar/components/sidebar_app.vue';
...@@ -74,17 +78,24 @@ ...@@ -74,17 +78,24 @@
required: false, required: false,
}, },
}, },
data() {
return {
// Epics specific configuration
issuableRef: '',
projectPath: this.groupPath,
projectNamespace: '',
};
},
components: { components: {
epicHeader, epicHeader,
epicSidebar, epicSidebar,
issuableApp, issuableApp,
relatedIssuesRoot, relatedIssuesRoot,
}, },
created() { methods: {
// Epics specific configuration deleteEpic() {
this.issuableRef = ''; issuableAppEventHub.$emit('delete.issuable');
this.projectPath = this.groupPath; },
this.projectNamespace = '';
}, },
}; };
</script> </script>
...@@ -94,6 +105,8 @@ ...@@ -94,6 +105,8 @@
<epic-header <epic-header
:author="author" :author="author"
:created="created" :created="created"
:canDelete="canDestroy"
@deleteEpic="deleteEpic"
/> />
<div class="issuable-details content-block"> <div class="issuable-details content-block">
<div class="detail-page-description"> <div class="detail-page-description">
...@@ -115,9 +128,21 @@ ...@@ -115,9 +128,21 @@
</div> </div>
<epic-sidebar <epic-sidebar
:endpoint="endpoint" :endpoint="endpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
:show-delete-button="false"
issuable-type="epic"
:editable="canUpdate" :editable="canUpdate"
:initialStartDate="startDate" :initial-start-date="startDate"
:initialEndDate="endDate" :initial-end-date="endDate"
/> />
<related-issues-root <related-issues-root
:endpoint="issueLinksEndpoint" :endpoint="issueLinksEndpoint"
......
...@@ -6,11 +6,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -6,11 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
const metaData = JSON.parse(el.dataset.meta); const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial); const initialData = JSON.parse(el.dataset.initial);
const props = Object.assign({}, initialData, metaData, { const props = Object.assign({}, initialData, metaData);
// Current iteration does not enable users
// to delete epics
canDestroy: false,
});
// Convert backend casing to match frontend style guide // Convert backend casing to match frontend style guide
props.startDate = props.start_date; props.startDate = props.start_date;
......
...@@ -27,13 +27,15 @@ module EE ...@@ -27,13 +27,15 @@ module EE
enable :create_epic enable :create_epic
enable :admin_epic enable :admin_epic
enable :update_epic enable :update_epic
enable :destroy_epic
end end
rule { owner }.enable :destroy_epic
rule { auditor }.policy do rule { auditor }.policy do
enable :read_group enable :read_group
enable :read_epic enable :read_epic
end end
rule { admin }.enable :read_epic rule { admin }.enable :read_epic
rule { has_projects }.enable :read_epic rule { has_projects }.enable :read_epic
......
module EE
module Users
module MigrateToGhostUserService
private
def migrate_records
migrate_epics
super
end
def migrate_epics
user.epics.update_all(author_id: ghost_user.id)
::Epic.where(last_edited_by_id: user.id).update_all(last_edited_by_id: ghost_user.id)
end
end
end
end
...@@ -91,7 +91,7 @@ describe Groups::EpicsController do ...@@ -91,7 +91,7 @@ describe Groups::EpicsController do
describe 'PUT #update' do describe 'PUT #update' do
before do before do
group.add_user(user, :developer) group.add_developer(user)
put :update, group_id: group, id: epic.to_param, epic: { title: 'New title' }, format: :json put :update, group_id: group, id: epic.to_param, epic: { title: 'New title' }, format: :json
end end
...@@ -107,7 +107,7 @@ describe Groups::EpicsController do ...@@ -107,7 +107,7 @@ describe Groups::EpicsController do
describe 'GET #realtime_changes' do describe 'GET #realtime_changes' do
subject { get :realtime_changes, group_id: group, id: epic.to_param } subject { get :realtime_changes, group_id: group, id: epic.to_param }
it 'returns epic' do it 'returns epic' do
group.add_user(user, :developer) group.add_developer(user)
subject subject
expect(response.content_type).to eq 'application/json' expect(response.content_type).to eq 'application/json'
...@@ -122,4 +122,25 @@ describe Groups::EpicsController do ...@@ -122,4 +122,25 @@ describe Groups::EpicsController do
end end
end end
end end
describe "DELETE #destroy" do
before do
sign_in(user)
end
it "rejects a developer to destroy an epic" do
group.add_developer(user)
delete :destroy, group_id: group, id: epic.to_param
expect(response).to have_gitlab_http_status(404)
end
it "deletes the epic" do
group.add_owner(user)
delete :destroy, group_id: group, id: epic.to_param
expect(response).to have_gitlab_http_status(302)
expect(controller).to set_flash[:notice].to(/The epic was successfully deleted\./)
end
end
end end
require 'spec_helper'
feature 'Delete Epic', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:epic) { create(:epic, group: group) }
let!(:epic2) { create(:epic, group: group) }
before do
sign_in(user)
end
context 'when user who is not a group member displays the epic' do
it 'does not show the Delete button' do
visit group_epic_path(group, epic)
expect(page).not_to have_selector('.detail-page-header button')
end
end
context 'when user with owner access displays the epic' do
before do
group.add_owner(user)
visit group_epic_path(group, epic)
wait_for_requests
end
it 'deletes the issue and redirect to epic list' do
page.accept_alert 'Epic will be removed! Are you sure?' do
find('.detail-page-header button').click
end
wait_for_requests
expect(find('.issuable-list')).not_to have_content(epic.title)
expect(find('.issuable-list')).to have_content(epic2.title)
end
end
end
...@@ -32,6 +32,14 @@ describe EpicPolicy do ...@@ -32,6 +32,14 @@ describe EpicPolicy do
it 'reporter group member can manage epics' do it 'reporter group member can manage epics' do
group.add_reporter(user) group.add_reporter(user)
expect(permissions(user, group)).to be_disallowed(:destroy_epic)
expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :admin_epic, :create_epic)
end
it 'only group owner can destroy epics' do
group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
...@@ -60,6 +68,14 @@ describe EpicPolicy do ...@@ -60,6 +68,14 @@ describe EpicPolicy do
it 'reporter group member can manage epics' do it 'reporter group member can manage epics' do
group.add_reporter(user) group.add_reporter(user)
expect(permissions(user, group)).to be_disallowed(:destroy_epic)
expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :admin_epic, :create_epic)
end
it 'only group owner can destroy epics' do
group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
...@@ -88,6 +104,14 @@ describe EpicPolicy do ...@@ -88,6 +104,14 @@ describe EpicPolicy do
it 'reporter group member can manage epics' do it 'reporter group member can manage epics' do
group.add_reporter(user) group.add_reporter(user)
expect(permissions(user, group)).to be_disallowed(:destroy_epic)
expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :admin_epic, :create_epic)
end
it 'only group owner can destroy epics' do
group.add_owner(user)
expect(permissions(user, group)) expect(permissions(user, group))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic) .to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end end
......
require 'spec_helper'
describe Users::MigrateToGhostUserService do
context 'epics' do
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
context 'deleted user is present as both author and edited_user' do
include_examples "migrating a deleted user's associated records to the ghost user", Epic, [:author, :last_edited_by] do
let(:created_record) do
create(:epic, group: create(:group), author: user, last_edited_by: user)
end
end
end
context 'deleted user is present only as edited_user' do
include_examples "migrating a deleted user's associated records to the ghost user", Epic, [:last_edited_by] do
let(:created_record) { create(:epic, group: create(:group), author: create(:user), last_edited_by: user) }
end
end
end
end
...@@ -31,4 +31,42 @@ describe('epicHeader', () => { ...@@ -31,4 +31,42 @@ describe('epicHeader', () => {
it('should render username tooltip', () => { it('should render username tooltip', () => {
expect(vm.$el.querySelector('.user-avatar-link span').dataset.originalTitle).toEqual(author.username); expect(vm.$el.querySelector('.user-avatar-link span').dataset.originalTitle).toEqual(author.username);
}); });
describe('canDelete', () => {
it('should not show loading button by default', () => {
expect(vm.$el.querySelector('.btn-remove')).toBeNull();
});
it('should show loading button if canDelete', (done) => {
vm.canDelete = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn-remove')).toBeDefined();
done();
});
});
});
describe('delete epic', () => {
let deleteEpic;
beforeEach((done) => {
deleteEpic = jasmine.createSpy();
spyOn(window, 'confirm').and.returnValue(true);
vm.canDelete = true;
vm.$on('deleteEpic', deleteEpic);
Vue.nextTick(() => {
vm.$el.querySelector('.btn-remove').click();
done();
});
});
it('should set deleteLoading', () => {
expect(vm.deleteLoading).toEqual(true);
});
it('should emit deleteEpic event', () => {
expect(deleteEpic).toHaveBeenCalled();
});
});
}); });
...@@ -3,6 +3,8 @@ import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue'; ...@@ -3,6 +3,8 @@ import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue'; import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue'; import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import issuableAppEventHub from '~/issue_show/event_hub';
import '~/lib/utils/url_utility';
import mountComponent from '../../../helpers/vue_mount_component_helper'; import mountComponent from '../../../helpers/vue_mount_component_helper';
import { props } from '../mock_data'; import { props } from '../mock_data';
import issueShowData from '../../../issue_show/mock_data'; import issueShowData from '../../../issue_show/mock_data';
...@@ -49,6 +51,7 @@ describe('EpicShowApp', () => { ...@@ -49,6 +51,7 @@ describe('EpicShowApp', () => {
headerVm = mountComponent(EpicHeader, { headerVm = mountComponent(EpicHeader, {
author, author,
created, created,
canDelete: canDestroy,
}); });
const IssuableApp = Vue.extend(issuableApp); const IssuableApp = Vue.extend(issuableApp);
...@@ -92,4 +95,14 @@ describe('EpicShowApp', () => { ...@@ -92,4 +95,14 @@ describe('EpicShowApp', () => {
it('should render epic-sidebar', () => { it('should render epic-sidebar', () => {
expect(vm.$el.innerHTML.indexOf(sidebarVm.$el.innerHTML) !== -1).toEqual(true); expect(vm.$el.innerHTML.indexOf(sidebarVm.$el.innerHTML) !== -1).toEqual(true);
}); });
it('should emit delete.issuable when epic is deleted', () => {
const deleteIssuable = jasmine.createSpy();
issuableAppEventHub.$on('delete.issuable', deleteIssuable);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(gl.utils, 'visitUrl').and.callFake(() => {});
vm.$el.querySelector('.detail-page-header .btn-remove').click();
expect(deleteIssuable).toHaveBeenCalled();
});
}); });
...@@ -2,7 +2,7 @@ export const contentProps = { ...@@ -2,7 +2,7 @@ export const contentProps = {
endpoint: '', endpoint: '',
canAdmin: true, canAdmin: true,
canUpdate: true, canUpdate: true,
canDestroy: false, canDestroy: true,
markdownPreviewPath: '', markdownPreviewPath: '',
markdownDocsPath: '', markdownDocsPath: '',
issueLinksEndpoint: '/', issueLinksEndpoint: '/',
......
...@@ -223,23 +223,46 @@ describe('Issuable output', () => { ...@@ -223,23 +223,46 @@ describe('Issuable output', () => {
}); });
}); });
it('closes form on error', (done) => { describe('error when updating', () => {
spyOn(window, 'Flash').and.callThrough(); beforeEach(() => {
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { spyOn(window, 'Flash').and.callThrough();
reject(); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
})); reject();
}));
});
vm.updateIssuable(); it('closes form on error', (done) => {
vm.updateIssuable();
setTimeout(() => { setTimeout(() => {
expect( expect(
eventHub.$emit, eventHub.$emit,
).toHaveBeenCalledWith('close.form'); ).toHaveBeenCalledWith('close.form');
expect( expect(
window.Flash, window.Flash,
).toHaveBeenCalledWith('Error updating issue'); ).toHaveBeenCalledWith('Error updating issue');
done(); done();
});
});
it('returns the correct error message for issuableType', (done) => {
vm.issuableType = 'merge request';
Vue.nextTick(() => {
vm.updateIssuable();
setTimeout(() => {
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
expect(
window.Flash,
).toHaveBeenCalledWith('Error updating merge request');
done();
});
});
}); });
}); });
}); });
......
...@@ -61,6 +61,15 @@ describe('Edit Actions components', () => { ...@@ -61,6 +61,15 @@ describe('Edit Actions components', () => {
}); });
}); });
it('should not show delete button if showDeleteButton is false', (done) => {
vm.showDeleteButton = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn-danger')).toBeNull();
done();
});
});
describe('updateIssuable', () => { describe('updateIssuable', () => {
it('sends update.issauble event when clicking save button', () => { it('sends update.issauble event when clicking save button', () => {
vm.$el.querySelector('.btn-save').click(); vm.$el.querySelector('.btn-save').click();
......
...@@ -66,6 +66,23 @@ describe('LoadingButton', function () { ...@@ -66,6 +66,23 @@ describe('LoadingButton', function () {
}); });
}); });
describe('container class', () => {
it('should default to btn btn-align-content', () => {
vm = mountComponent(LoadingButton, {});
expect(vm.$el.classList.contains('btn')).toEqual(true);
expect(vm.$el.classList.contains('btn-align-content')).toEqual(true);
});
it('should be configurable through props', () => {
vm = mountComponent(LoadingButton, {
containerClass: 'test-class',
});
expect(vm.$el.classList.contains('btn')).toEqual(false);
expect(vm.$el.classList.contains('btn-align-content')).toEqual(false);
expect(vm.$el.classList.contains('test-class')).toEqual(true);
});
});
describe('click callback prop', () => { describe('click callback prop', () => {
it('calls given callback when normal', () => { it('calls given callback when normal', () => {
vm = mountComponent(LoadingButton, { vm = mountComponent(LoadingButton, {
......
...@@ -15,4 +15,13 @@ describe Geo::FileRegistry do ...@@ -15,4 +15,13 @@ describe Geo::FileRegistry do
expect(described_class.synced).to contain_exactly(synced) expect(described_class.synced).to contain_exactly(synced)
end end
end end
describe '.retry_due' do
set(:retry_yesterday) { create(:geo_file_registry, retry_at: Date.yesterday) }
set(:retry_tomorrow) { create(:geo_file_registry, retry_at: Date.tomorrow) }
it 'returns registries in the synced state' do
expect(described_class.retry_due).not_to contain_exactly([retry_tomorrow])
end
end
end end
...@@ -38,6 +38,16 @@ describe Geo::ProjectRegistry do ...@@ -38,6 +38,16 @@ describe Geo::ProjectRegistry do
end end
end end
describe '.retry_due' do
it 'returns projects that should be synced' do
create(:geo_project_registry, repository_retry_at: Date.yesterday, wiki_retry_at: Date.yesterday)
tomorrow = create(:geo_project_registry, repository_retry_at: Date.tomorrow, wiki_retry_at: Date.tomorrow)
create(:geo_project_registry)
expect(described_class.retry_due).not_to include(tomorrow)
end
end
describe '#repository_sync_due?' do describe '#repository_sync_due?' do
where(:resync_repository, :last_successful_sync, :last_sync, :expected) do where(:resync_repository, :last_successful_sync, :last_sync, :expected) do
now = Time.now now = Time.now
......
...@@ -26,6 +26,7 @@ describe GroupPolicy do ...@@ -26,6 +26,7 @@ describe GroupPolicy do
:admin_namespace, :admin_namespace,
:admin_group_member, :admin_group_member,
:change_visibility_level, :change_visibility_level,
:destroy_epic,
(Gitlab::Database.postgresql? ? :create_subgroup : nil) (Gitlab::Database.postgresql? ? :create_subgroup : nil)
].compact ].compact
end end
......
...@@ -9,10 +9,10 @@ describe Geo::BaseSyncService do ...@@ -9,10 +9,10 @@ describe Geo::BaseSyncService do
describe '#lease_key' do describe '#lease_key' do
it 'returns a key in the correct pattern' do it 'returns a key in the correct pattern' do
described_class.type = :test allow(described_class).to receive(:type) { :wiki }
allow(project).to receive(:id) { 999 } allow(project).to receive(:id) { 999 }
expect(subject.lease_key).to eq('geo_sync_service:test:999') expect(subject.lease_key).to eq('geo_sync_service:wiki:999')
end end
end end
......
...@@ -29,6 +29,8 @@ describe Geo::FileDownloadService do ...@@ -29,6 +29,8 @@ describe Geo::FileDownloadService do
stub_transfer(Gitlab::Geo::FileTransfer, -1) stub_transfer(Gitlab::Geo::FileTransfer, -1)
expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1) expect { execute! }.to change { Geo::FileRegistry.failed.count }.by(1)
expect(Geo::FileRegistry.last.retry_count).to eq(1)
expect(Geo::FileRegistry.last.retry_at).to be_present
end end
it 'registers when the download fails with some other error' do it 'registers when the download fails with some other error' do
......
require 'spec_helper' require 'spec_helper'
RSpec.describe Geo::RepositorySyncService do describe Geo::RepositorySyncService do
include ::EE::GeoHelpers include ::EE::GeoHelpers
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node', relative_url_root: '/gitlab') } set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node', relative_url_root: '/gitlab') }
...@@ -122,6 +122,15 @@ RSpec.describe Geo::RepositorySyncService do ...@@ -122,6 +122,15 @@ RSpec.describe Geo::RepositorySyncService do
subject.execute subject.execute
end end
it 'sets repository_retry_count and repository_retry_at to nil' do
registry = create(:geo_project_registry, project: project, repository_retry_count: 2, repository_retry_at: Date.yesterday)
subject.execute
expect(registry.reload.repository_retry_count).to be_nil
expect(registry.repository_retry_at).to be_nil
end
end end
context 'when repository sync fail' do context 'when repository sync fail' do
...@@ -140,6 +149,45 @@ RSpec.describe Geo::RepositorySyncService do ...@@ -140,6 +149,45 @@ RSpec.describe Geo::RepositorySyncService do
it 'resets last_repository_successful_sync_at' do it 'resets last_repository_successful_sync_at' do
expect(registry.last_repository_successful_sync_at).to be_nil expect(registry.last_repository_successful_sync_at).to be_nil
end end
it 'resets repository_retry_count' do
expect(registry.repository_retry_count).to eq(1)
end
it 'resets repository_retry_at' do
expect(registry.repository_retry_at).to be_present
end
end
end
context 'retries' do
it 'tries to fetch repo' do
create(:geo_project_registry, project: project, repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD - 1)
expect_any_instance_of(described_class).to receive(:fetch_project_repository).with(false)
subject.execute
end
it 'tries to redownload repo' do
create(:geo_project_registry, project: project, repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD + 1)
expect_any_instance_of(described_class).to receive(:fetch_project_repository).with(true)
subject.execute
end
it 'tries to redownload repo when force_redownload flag is set' do
create(
:geo_project_registry,
project: project,
repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD - 1,
force_to_redownload_repository: true
)
expect_any_instance_of(described_class).to receive(:fetch_project_repository).with(true)
subject.execute
end end
end end
......
...@@ -8,6 +8,7 @@ shared_examples 'geo base sync execution' do ...@@ -8,6 +8,7 @@ shared_examples 'geo base sync execution' do
end end
it 'executes the synchronization' do it 'executes the synchronization' do
subject.class.type ||= :wiki
expect(subject).to receive(:sync_repository) expect(subject).to receive(:sync_repository)
subject.execute subject.execute
......
...@@ -96,7 +96,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -96,7 +96,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
end end
context 'with a failed file' do context 'with a failed file' do
let!(:failed_registry) { create(:geo_file_registry, :lfs, file_id: 999, success: false) } let(:failed_registry) { create(:geo_file_registry, :lfs, file_id: 999, success: false) }
it 'does not stall backfill' do it 'does not stall backfill' do
unsynced = create(:lfs_object, :with_file) unsynced = create(:lfs_object, :with_file)
...@@ -114,6 +114,22 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -114,6 +114,22 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
subject.perform subject.perform
end end
it 'does not retries failed files when retry_at is tomorrow' do
failed_registry = create(:geo_file_registry, :lfs, file_id: 999, success: false, retry_at: Date.tomorrow)
expect(GeoFileDownloadWorker).not_to receive(:perform_async).with('lfs', failed_registry.file_id)
subject.perform
end
it 'does not retries failed files when retry_at is in the past' do
failed_registry = create(:geo_file_registry, :lfs, file_id: 999, success: false, retry_at: Date.yesterday)
expect(GeoFileDownloadWorker).to receive(:perform_async).with('lfs', failed_registry.file_id)
subject.perform
end
end end
context 'when node has namespace restrictions' do context 'when node has namespace restrictions' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment