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:
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
enabled: false
enabled: true
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
......
......@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableRef: {
type: String,
required: true,
......@@ -92,6 +97,11 @@ export default {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
data() {
const store = new Store({
......@@ -157,21 +167,21 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash('Error updating issue');
window.Flash(`Error updating ${this.issuableType}`);
});
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.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();
gl.utils.visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash('Error deleting issue');
window.Flash(`Error deleting ${this.issuableType}`);
});
},
},
......@@ -223,6 +233,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
/>
<div v-else>
<title-component
......
......@@ -13,6 +13,11 @@
type: Object,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -23,6 +28,9 @@
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
},
methods: {
closeForm() {
......@@ -62,7 +70,7 @@
Cancel
</button>
<button
v-if="canDestroy"
v-if="shouldShowDeleteButton"
class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
......
......@@ -36,6 +36,11 @@
type: String,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
},
components: {
lockedWarning,
......@@ -81,6 +86,7 @@
:markdown-docs-path="markdownDocsPath" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
:can-destroy="canDestroy"
:show-delete-button="showDeleteButton" />
</form>
</template>
......@@ -35,6 +35,11 @@ export default {
type: String,
required: false,
},
containerClass: {
type: String,
required: false,
default: 'btn btn-align-content',
},
},
components: {
loadingIcon,
......@@ -49,9 +54,9 @@ export default {
<template>
<button
class="btn btn-align-content"
@click="onClick"
type="button"
:class="containerClass"
:disabled="loading || disabled"
>
<transition name="fade">
......
......@@ -353,3 +353,7 @@
display: -webkit-flex;
display: flex;
}
.flex-right {
margin-left: auto;
}
......@@ -101,13 +101,13 @@
@for $i from 0 through 5 {
.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 {
.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 @@
@for $i from 0 through 5 {
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 {
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;
$gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494;
$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-green: $green-600;
$gl-text-green-hover: $green-700;
......@@ -493,8 +493,8 @@ $callout-success-color: $green-700;
/*
* Commit Page
*/
$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
$commit-message-text-area-bg: 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);
/*
* Common
......
......@@ -333,7 +333,7 @@
.prometheus-graph-overlay {
fill: none;
opacity: 0.0;
opacity: 0;
pointer-events: all;
}
......
......@@ -715,13 +715,16 @@
.approvals-footer {
display: flex;
.approvers-prefix,
.approvers-list {
.approvers-prefix {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.approvers-list {
display: flex;
align-items: center;
.link-to-member-avatar:not(:first-child) {
img {
margin-left: 0;
......
......@@ -7,7 +7,7 @@
.diff-file .diff-content {
tr.line_holder:hover > td .line_note_link {
opacity: 1.0;
opacity: 1;
filter: alpha(opacity = 100);
}
}
......
class Geo::FileRegistry < Geo::BaseRegistry
scope :failed, -> { where(success: false) }
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 :attachments, -> { where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES) }
end
......@@ -15,6 +15,15 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
where(repository_sync_failed.or(wiki_sync_failed))
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
where.not(last_repository_synced_at: nil, last_repository_successful_sync_at: nil)
.where(resync_repository: false, resync_wiki: false)
......@@ -39,10 +48,16 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
end
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
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
require 'securerandom'
module Geo
# 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
......@@ -6,6 +8,7 @@ module Geo
class BaseSyncService
include ExclusiveLeaseGuard
include ::Gitlab::Geo::ProjectLogHelpers
include Delay
class << self
attr_accessor :type
......@@ -15,6 +18,8 @@ module Geo
LEASE_TIMEOUT = 8.hours.freeze
LEASE_KEY_PREFIX = 'geo_sync_service'.freeze
RETRY_BEFORE_REDOWNLOAD = 5
RETRY_LIMIT = 8
def initialize(project)
@project = project
......@@ -23,7 +28,18 @@ module Geo
def execute
try_obtain_lease do
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")
end
end
......@@ -48,6 +64,22 @@ module Geo
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
raise NotImplementedError, 'This class should implement sync_repository method'
end
......@@ -101,11 +133,17 @@ module Geo
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
attrs["last_#{type}_successful_sync_at"] = finished_at
attrs["resync_#{type}"] = false
attrs["#{type}_retry_count"] = nil
attrs["#{type}_retry_at"] = nil
end
registry.update!(attrs)
......@@ -134,5 +172,43 @@ module Geo
def last_synced_at
registry.public_send("last_#{type}_synced_at") # rubocop:disable GitlabSecurity/PublicSend
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
......@@ -2,6 +2,8 @@ module Geo
class FileDownloadService < FileService
LEASE_TIMEOUT = 8.hours.freeze
include Delay
def execute
try_obtain_lease do |lease|
start_time = Time.now
......@@ -45,6 +47,13 @@ module Geo
transfer.bytes = bytes_downloaded
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
end
......
module Geo
class RepositorySyncService < BaseSyncService
include Gitlab::ShellAdapter
self.type = :repository
private
def sync_repository
fetch_project_repository
def sync_repository(redownload = false)
fetch_project_repository(redownload)
expire_repository_caches
end
def fetch_project_repository
log_info('Fetching project repository')
def fetch_project_repository(redownload)
log_info('Trying to fetch project repository')
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
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
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
def expire_repository_caches
......@@ -40,5 +50,9 @@ module Geo
def ssh_url_to_repo
"#{primary_ssh_path_prefix}#{project.full_path}.git"
end
def repository
project.repository
end
end
end
module Geo
class WikiSyncService < BaseSyncService
include Gitlab::ShellAdapter
self.type = :wiki
private
def sync_repository
fetch_wiki_repository
def sync_repository(redownload = false)
fetch_wiki_repository(redownload)
end
def fetch_wiki_repository
def fetch_wiki_repository(redownload)
log_info('Fetching wiki repository')
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
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
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
def ssh_url_to_wiki
"#{primary_ssh_path_prefix}#{project.full_path}.wiki.git"
end
def repository
project.wiki.repository
end
end
end
......@@ -6,6 +6,7 @@
# when the user is destroyed.
module Users
class MigrateToGhostUserService
prepend EE::Users::MigrateToGhostUserService
extend ActiveSupport::Concern
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
def find_failed_objects(batch_size:)
Geo::FileRegistry
.failed
.retry_due
.limit(batch_size)
.pluck(:file_id, :file_type)
end
......
......@@ -33,6 +33,7 @@ module Geo
def find_project_ids_updated_recently(batch_size:)
current_node.project_registries
.dirty
.retry_due
.order(Gitlab::Database.nulls_first_order(:last_repository_synced_at, :desc))
.limit(batch_size)
.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 @@
- container_memory_usage_bytes
weight: 1
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
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
unit: MB
track: canary
......@@ -158,10 +158,10 @@
- container_cpu_usage_seconds_total
weight: 1
queries:
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100'
label: CPU
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
label: Average
unit: "%"
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) * 100'
label: CPU
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) without (job)) * 100'
label: Average
unit: "%"
track: canary
\ No newline at end of file
track: canary
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -26,10 +26,13 @@ ActiveRecord::Schema.define(version: 20171009162209) do
t.string "sha256"
t.datetime "created_at", null: false
t.boolean "success", default: false, null: false
t.integer "retry_count"
t.datetime "retry_at"
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"], 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
create_table "project_registry", force: :cascade do |t|
......@@ -41,12 +44,20 @@ ActiveRecord::Schema.define(version: 20171009162209) do
t.boolean "resync_wiki", default: true, null: false
t.datetime "last_wiki_synced_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
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", ["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_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
......@@ -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_PASSWORD_HASH`. The md5 hash of POSTGRESQL_USER_PASSWORD
#### Pgbouncer
When using default setup, minimum configuration requires:
......@@ -231,7 +232,6 @@ See `START user configuration` section in the next step for required information
postgresql['hot_standby'] = 'on'
postgresql['wal_level'] = 'replica'
postgresql['shared_preload_libraries'] = 'repmgr_funcs'
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
# Disable automatic database migrations
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
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)
[PostgreSQL](http://docs.gitlab.com/omnibus/settings/database.html#troubleshooting)
[GitLab application](gitlab.md#troubleshooting)
- [Consul](consul.md#troubleshooting)
- [PostgreSQL](http://docs.gitlab.com/omnibus/settings/database.html#troubleshooting)
- [GitLab application](gitlab.md#troubleshooting)
---
......
# Working with the bundle Consul service
# Working with the bundled Consul service
## Overview
......
......@@ -13,8 +13,8 @@ integration services must be enabled.
| 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 CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 |
| 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(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
......
......@@ -2,6 +2,8 @@
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import { s__ } from '~/locale';
export default {
name: 'epicHeader',
......@@ -15,6 +17,16 @@
type: String,
required: true,
},
canDelete: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
deleteLoading: false,
};
},
directives: {
tooltip,
......@@ -22,6 +34,15 @@
components: {
userAvatarLink,
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>
......@@ -29,21 +50,26 @@
<template>
<div class="detail-page-header">
<div class="issuable-meta">
Opened
<timeagoTooltip
:time="created"
/>
by
{{ s__('Opened') }}
<timeago-tooltip :time="created" />
{{ s__('by') }}
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltipText="author.username"
:tooltip-text="author.username"
:username="author.name"
imgCssClasses="avatar-inline"
img-css-classes="avatar-inline"
/>
</strong>
</div>
<loading-button
v-if="canDelete"
:loading="deleteLoading"
@click="deleteEpic"
:label="s__('Delete')"
container-class="btn btn-remove btn-inverted flex-right"
/>
</div>
</template>
<script>
import issuableApp from '~/issue_show/components/app.vue';
<<<<<<< HEAD
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 epicSidebar from '../../sidebar/components/sidebar_app.vue';
......@@ -74,17 +78,24 @@
required: false,
},
},
data() {
return {
// Epics specific configuration
issuableRef: '',
projectPath: this.groupPath,
projectNamespace: '',
};
},
components: {
epicHeader,
epicSidebar,
issuableApp,
relatedIssuesRoot,
},
created() {
// Epics specific configuration
this.issuableRef = '';
this.projectPath = this.groupPath;
this.projectNamespace = '';
methods: {
deleteEpic() {
issuableAppEventHub.$emit('delete.issuable');
},
},
};
</script>
......@@ -94,6 +105,8 @@
<epic-header
:author="author"
:created="created"
:canDelete="canDestroy"
@deleteEpic="deleteEpic"
/>
<div class="issuable-details content-block">
<div class="detail-page-description">
......@@ -115,9 +128,21 @@
</div>
<epic-sidebar
: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"
:initialStartDate="startDate"
:initialEndDate="endDate"
:initial-start-date="startDate"
:initial-end-date="endDate"
/>
<related-issues-root
:endpoint="issueLinksEndpoint"
......
......@@ -6,11 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial);
const props = Object.assign({}, initialData, metaData, {
// Current iteration does not enable users
// to delete epics
canDestroy: false,
});
const props = Object.assign({}, initialData, metaData);
// Convert backend casing to match frontend style guide
props.startDate = props.start_date;
......
......@@ -27,13 +27,15 @@ module EE
enable :create_epic
enable :admin_epic
enable :update_epic
enable :destroy_epic
end
rule { owner }.enable :destroy_epic
rule { auditor }.policy do
enable :read_group
enable :read_epic
end
rule { admin }.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
describe 'PUT #update' 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
end
......@@ -107,7 +107,7 @@ describe Groups::EpicsController do
describe 'GET #realtime_changes' do
subject { get :realtime_changes, group_id: group, id: epic.to_param }
it 'returns epic' do
group.add_user(user, :developer)
group.add_developer(user)
subject
expect(response.content_type).to eq 'application/json'
......@@ -122,4 +122,25 @@ describe Groups::EpicsController do
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
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
it 'reporter group member can manage epics' do
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))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
......@@ -60,6 +68,14 @@ describe EpicPolicy do
it 'reporter group member can manage epics' do
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))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
end
......@@ -88,6 +104,14 @@ describe EpicPolicy do
it 'reporter group member can manage epics' do
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))
.to be_allowed(:read_epic, :update_epic, :destroy_epic, :admin_epic, :create_epic)
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', () => {
it('should render username tooltip', () => {
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';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_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 { props } from '../mock_data';
import issueShowData from '../../../issue_show/mock_data';
......@@ -49,6 +51,7 @@ describe('EpicShowApp', () => {
headerVm = mountComponent(EpicHeader, {
author,
created,
canDelete: canDestroy,
});
const IssuableApp = Vue.extend(issuableApp);
......@@ -92,4 +95,14 @@ describe('EpicShowApp', () => {
it('should render epic-sidebar', () => {
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 = {
endpoint: '',
canAdmin: true,
canUpdate: true,
canDestroy: false,
canDestroy: true,
markdownPreviewPath: '',
markdownDocsPath: '',
issueLinksEndpoint: '/',
......
......@@ -223,23 +223,46 @@ describe('Issuable output', () => {
});
});
it('closes form on error', (done) => {
spyOn(window, 'Flash').and.callThrough();
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
reject();
}));
describe('error when updating', () => {
beforeEach(() => {
spyOn(window, 'Flash').and.callThrough();
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
reject();
}));
});
vm.updateIssuable();
it('closes form on error', (done) => {
vm.updateIssuable();
setTimeout(() => {
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
expect(
window.Flash,
).toHaveBeenCalledWith('Error updating issue');
setTimeout(() => {
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
expect(
window.Flash,
).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', () => {
});
});
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', () => {
it('sends update.issauble event when clicking save button', () => {
vm.$el.querySelector('.btn-save').click();
......
......@@ -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', () => {
it('calls given callback when normal', () => {
vm = mountComponent(LoadingButton, {
......
......@@ -15,4 +15,13 @@ describe Geo::FileRegistry do
expect(described_class.synced).to contain_exactly(synced)
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
......@@ -38,6 +38,16 @@ describe Geo::ProjectRegistry do
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
where(:resync_repository, :last_successful_sync, :last_sync, :expected) do
now = Time.now
......
......@@ -26,6 +26,7 @@ describe GroupPolicy do
:admin_namespace,
:admin_group_member,
:change_visibility_level,
:destroy_epic,
(Gitlab::Database.postgresql? ? :create_subgroup : nil)
].compact
end
......
......@@ -9,10 +9,10 @@ describe Geo::BaseSyncService do
describe '#lease_key' 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 }
expect(subject.lease_key).to eq('geo_sync_service:test:999')
expect(subject.lease_key).to eq('geo_sync_service:wiki:999')
end
end
......
......@@ -29,6 +29,8 @@ describe Geo::FileDownloadService do
stub_transfer(Gitlab::Geo::FileTransfer, -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
it 'registers when the download fails with some other error' do
......
require 'spec_helper'
RSpec.describe Geo::RepositorySyncService do
describe Geo::RepositorySyncService do
include ::EE::GeoHelpers
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node', relative_url_root: '/gitlab') }
......@@ -122,6 +122,15 @@ RSpec.describe Geo::RepositorySyncService do
subject.execute
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
context 'when repository sync fail' do
......@@ -140,6 +149,45 @@ RSpec.describe Geo::RepositorySyncService do
it 'resets last_repository_successful_sync_at' do
expect(registry.last_repository_successful_sync_at).to be_nil
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
......
......@@ -8,6 +8,7 @@ shared_examples 'geo base sync execution' do
end
it 'executes the synchronization' do
subject.class.type ||= :wiki
expect(subject).to receive(:sync_repository)
subject.execute
......
......@@ -96,7 +96,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
end
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
unsynced = create(:lfs_object, :with_file)
......@@ -114,6 +114,22 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
subject.perform
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
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