提交 5629d98f 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 1b26aaa6
......@@ -53,7 +53,7 @@ After your merge request has been approved according to our [approval guidelines
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
| GitLab EE only | Yes/No | |
| GitLab EE only | Yes/No | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
......@@ -62,7 +62,6 @@ After your merge request has been approved according to our [approval guidelines
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/secpick_script.md
[security Release merge request template]: https://gitlab.com/gitlab-org/security/gitlab/blob/master/.gitlab/merge_request_templates/Security%20Release.md
[code review process]: https://docs.gitlab.com/ee/development/code_review.html
[approval guidelines]: https://docs.gitlab.com/ee/development/code_review.html#approval-guidelines
[issue as related]: https://docs.gitlab.com/ee/user/project/issues/related_issues.html#adding-a-related-issue
......
3876ecd3e4f6bf756621ad07de5e033f8a5b6129
40b90823b0d55561059d27249e02db426b428786
......@@ -106,7 +106,6 @@ export default {
},
},
mounted() {
this.toggleOnPasteListener(this.$route.name);
if (this.$route.path === '/designs') {
this.$el.scrollIntoView();
}
......
......@@ -213,7 +213,7 @@ export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
* This technical debt is being tracked here
* https://gitlab.com/gitlab-org/gitlab/-/issues/214671
*/
export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
export const OVERVIEW_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
/**
* GitLab provide metrics dashboards that are available to a user once
......
......@@ -17,7 +17,7 @@ import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { getDashboard, getPrometheusQueryData, getPanelJson } from '../requests';
import { ENVIRONMENT_AVAILABLE_STATE, DEFAULT_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
......@@ -298,7 +298,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
export const fetchAnnotations = ({ state, dispatch, getters }) => {
const { start } = convertToFixedRange(state.timeRange);
const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getAnnotations,
......@@ -331,12 +331,12 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => {
/**
* Normally, the default dashboard won't throw any validation warnings.
* Normally, the overview dashboard won't throw any validation warnings.
*
* However, if a bug sneaks into the default dashboard making it invalid,
* However, if a bug sneaks into the overview dashboard making it invalid,
* this might come handy for our clients
*/
const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getDashboardValidationWarnings,
......
......@@ -465,9 +465,9 @@ export const addPrefixToCustomVariableParams = name => `variables[${name}]`;
* metrics dashboard to work with custom dashboard file names instead
* of the entire path.
*
* If dashboard is empty, it is the default dashboard.
* If dashboard is empty, it is the overview dashboard.
* If dashboard is set, it usually is a custom dashboard unless
* explicitly it is set to default dashboard path.
* explicitly it is set to overview dashboard path.
*
* @param {String} dashboard dashboard path
* @param {String} dashboardPrefix custom dashboard directory prefix
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
......@@ -10,6 +9,7 @@ import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
import TagField from './tag_field.vue';
export default {
name: 'ReleaseEditNewApp',
......@@ -20,6 +20,7 @@ export default {
MarkdownField,
AssetLinksForm,
MilestoneCombobox,
TagField,
},
directives: {
autofocusonshow,
......@@ -55,23 +56,6 @@ export default {
false,
);
},
tagName() {
return this.$store.state.detail.release.tagName;
},
tagNameHintText() {
return sprintf(
__(
'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
),
{
linkStart: `<a href="${escape(
this.updateReleaseApiDocsPath,
)}" target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
},
false,
);
},
releaseTitle: {
get() {
return this.$store.state.detail.release.name;
......@@ -136,22 +120,7 @@ export default {
<div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
<form v-if="showForm" @submit.prevent="updateRelease()">
<gl-form-group>
<div class="row">
<div class="col-md-6 col-lg-5 col-xl-4">
<label for="git-ref">{{ __('Tag name') }}</label>
<gl-form-input
id="git-ref"
v-model="tagName"
type="text"
class="form-control"
aria-describedby="tag-name-help"
disabled
/>
</div>
</div>
<div id="tag-name-help" class="form-text text-muted" v-html="tagNameHintText"></div>
</gl-form-group>
<tag-field />
<gl-form-group>
<label for="release-title">{{ __('Release title') }}</label>
<gl-form-input
......
<script>
import { mapGetters } from 'vuex';
import TagFieldExisting from './tag_field_existing.vue';
import TagFieldNew from './tag_field_new.vue';
export default {
components: {
TagFieldExisting,
TagFieldNew,
},
computed: {
...mapGetters('detail', ['isExistingRelease']),
},
};
</script>
<template>
<tag-field-existing v-if="isExistingRelease" />
<tag-field-new v-else />
</template>
<script>
import { mapState } from 'vuex';
import { uniqueId } from 'lodash';
import { GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
export default {
name: 'TagFieldExisting',
components: { GlFormGroup, GlFormInput, GlSprintf, GlLink },
computed: {
...mapState('detail', ['release', 'updateReleaseApiDocsPath']),
inputId() {
return uniqueId('tag-name-input-');
},
helpId() {
return uniqueId('tag-name-help-');
},
},
};
</script>
<template>
<gl-form-group :label="__('Tag name')" :label-for="inputId">
<div class="row">
<div class="col-md-6 col-lg-5 col-xl-4">
<gl-form-input
:id="inputId"
:value="release.tagName"
type="text"
class="form-control"
:aria-describedby="helpId"
disabled
/>
</div>
</div>
<template #description>
<div :id="helpId" data-testid="tag-name-help">
<gl-sprintf
:message="
__(
'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="updateReleaseApiDocsPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
</template>
</gl-form-group>
</template>
<script>
export default {
name: 'TagFieldNew',
};
</script>
<template>
<div></div>
</template>
import { isEmpty } from 'lodash';
import { hasContent } from '~/lib/utils/text_utility';
/**
* @returns {Boolean} `true` if the app is editing an existing release.
* `false` if the app is creating a new release.
*/
export const isExistingRelease = state => {
return Boolean(state.originalRelease);
};
/**
* @param {Object} link The link to test
* @returns {Boolean} `true` if the release link is empty, i.e. it has
......
......@@ -19,7 +19,12 @@ export default ({
manageMilestonesPath,
newMilestonePath,
/**
* The name of the tag associated with the release, provided by the backend.
* When creating a new release, this value is null.
*/
tagName,
releasesPagePath,
defaultBranch,
......
......@@ -19,14 +19,13 @@ module CommitStatusEnums
scheduler_failure: 11,
data_integrity_failure: 12,
forward_deployment_failure: 13,
protected_environment_failure: 1_000,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
upstream_bridge_project_not_found: 1_004,
insufficient_upstream_permissions: 1_005,
bridge_pipeline_is_child_pipeline: 1_006,
downstream_pipeline_creation_failed: 1_007
}
end
end
CommitStatusEnums.prepend_if_ee('EE::CommitStatusEnums')
# frozen_string_literal: true
# Add capabilities to increment a numeric model attribute efficiently by
# using Redis and flushing the increments asynchronously to the database
# after a period of time (10 minutes).
# When an attribute is incremented by a value, the increment is added
# to a Redis key. Then, FlushCounterIncrementsWorker will execute
# `flush_increments_to_database!` which removes increments from Redis for a
# given model attribute and updates the values in the database.
#
# @example:
#
# class ProjectStatistics
# include CounterAttribute
#
# counter_attribute :commit_count
# counter_attribute :storage_size
# end
#
# To increment the counter we can use the method:
# delayed_increment_counter(:commit_count, 3)
#
module CounterAttribute
extend ActiveSupport::Concern
extend AfterCommitQueue
include Gitlab::ExclusiveLeaseHelpers
LUA_STEAL_INCREMENT_SCRIPT = <<~EOS.freeze
local increment_key, flushed_key = KEYS[1], KEYS[2]
local increment_value = redis.call("get", increment_key) or 0
local flushed_value = redis.call("incrby", flushed_key, increment_value)
if flushed_value == 0 then
redis.call("del", increment_key, flushed_key)
else
redis.call("del", increment_key)
end
return flushed_value
EOS
WORKER_DELAY = 10.minutes
WORKER_LOCK_TTL = 10.minutes
class_methods do
def counter_attribute(attribute)
counter_attributes << attribute
end
def counter_attributes
@counter_attributes ||= Set.new
end
end
# This method must only be called by FlushCounterIncrementsWorker
# because it should run asynchronously and with exclusive lease.
# This will
# 1. temporarily move the pending increment for a given attribute
# to a relative "flushed" Redis key, delete the increment key and return
# the value. If new increments are performed at this point, the increment
# key is recreated as part of `delayed_increment_counter`.
# The "flushed" key is used to ensure that we can keep incrementing
# counters in Redis while flushing existing values.
# 2. then the value is used to update the counter in the database.
# 3. finally the "flushed" key is deleted.
def flush_increments_to_database!(attribute)
lock_key = counter_lock_key(attribute)
with_exclusive_lease(lock_key) do
increment_key = counter_key(attribute)
flushed_key = counter_flushed_key(attribute)
increment_value = steal_increments(increment_key, flushed_key)
next if increment_value == 0
transaction do
unsafe_update_counters(id, attribute => increment_value)
redis_state { |redis| redis.del(flushed_key) }
end
end
end
def delayed_increment_counter(attribute, increment)
return if increment == 0
run_after_commit_or_now do
if counter_attribute_enabled?(attribute)
redis_state do |redis|
redis.incrby(counter_key(attribute), increment)
end
FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
else
legacy_increment!(attribute, increment)
end
end
true
end
def counter_key(attribute)
"project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}"
end
def counter_flushed_key(attribute)
counter_key(attribute) + ':flushed'
end
def counter_lock_key(attribute)
counter_key(attribute) + ':lock'
end
private
def counter_attribute_enabled?(attribute)
Feature.enabled?(:efficient_counter_attribute, project) &&
self.class.counter_attributes.include?(attribute)
end
def steal_increments(increment_key, flushed_key)
redis_state do |redis|
redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
end
end
def legacy_increment!(attribute, increment)
increment!(attribute, increment)
end
def unsafe_update_counters(id, increments)
self.class.update_counters(id, increments)
end
def redis_state(&block)
Gitlab::Redis::SharedState.with(&block)
end
def with_exclusive_lease(lock_key)
in_lock(lock_key, ttl: WORKER_LOCK_TTL) do
yield
end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
# a worker is already updating the counters
end
end
......@@ -6,10 +6,10 @@ module Metrics
module Dashboard
class SelfMonitoringDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml'
DASHBOARD_NAME = N_('Default dashboard')
DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '1dff3e3cb76e73c8e368823c98b34c61aec0d141978450dea195a3b3dc2415d6'
DASHBOARD_VERSION = '0f7ade2022e09f1a1da8e883cc95d84b9557e1e0e9b015c51eb964296aa73098'
SEQUENCE = [
STAGES::CustomMetricsInserter,
......
......@@ -6,7 +6,7 @@ module Metrics
module Dashboard
class SystemDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
DASHBOARD_NAME = N_('Default dashboard')
DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
......
......@@ -1340,6 +1340,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: flush_counter_increments
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: git_garbage_collect
:feature_category: :gitaly
:has_external_dependencies:
......
# frozen_string_literal: true
# Invoked by CounterAttribute concern when incrementing counter
# attributes. The method `flush_increments_to_database!` that
# this worker uses is itself idempotent as it runs with exclusive
# lease to ensure that only one instance at the time can flush
# increments from Redis to the database.
class FlushCounterIncrementsWorker
include ApplicationWorker
feature_category_not_owned!
urgency :low
deduplicate :until_executing, including_scheduled: true
idempotent!
def perform(model_name, model_id, attribute)
return unless self.class.const_defined?(model_name)
model_class = model_name.constantize
model = model_class.find_by_id(model_id)
return unless model
model.flush_increments_to_database!(attribute)
end
end
---
title: Enforce namespace storage limit via app setting
merge_request: 38094
author:
type: changed
---
title: Resolve Pasting an image into a comment still uploades a design
merge_request: 38280
author:
type: fixed
---
title: In metrics view, change default dashboard name to Overview
merge_request: 38292
author:
type: changed
---
title: Making component diagram click-friendly
merge_request: 37147
author: Arjun Pravin @Sgt.Arjun
type: other
---
title: Add mechanism that efficiently increments ActiveRecord counters using Redis
merge_request: 35878
author:
type: performance
dashboard: 'Default dashboard'
dashboard: 'Overview'
priority: 1
templating:
......
......@@ -108,6 +108,8 @@
- 1
- - file_hook
- 1
- - flush_counter_increments
- 1
- - gcp_cluster
- 1
- - geo
......
# frozen_string_literal: true
class AddEnforceNamespaceStorageLimitToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :enforce_namespace_storage_limit, :boolean, default: false, null: false
end
end
a3a6d4e488c9979efd61890a15fdfe4ccea044a0b030b392ad39885cc807f22d
\ No newline at end of file
......@@ -9248,6 +9248,7 @@ CREATE TABLE public.application_settings (
maintenance_mode_message text,
wiki_page_max_content_bytes bigint DEFAULT 52428800 NOT NULL,
elasticsearch_indexed_file_size_limit_kb integer DEFAULT 1024 NOT NULL,
enforce_namespace_storage_limit boolean DEFAULT false NOT NULL,
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
......
......@@ -14,6 +14,11 @@ Find more about them [in Audit Events documentation](audit_events.md).
System log files are typically plain text in a standard log file format.
This guide talks about how to read and use these system log files.
[Read more about how to customise logging on Omnibus GitLab
installations](https://docs.gitlab.com/omnibus/settings/logs.html)
including adjusting log retention, log forwarding,
switching logs from JSON to plain text logging, and more.
## `production_json.log`
This file lives in `/var/log/gitlab/gitlab-rails/production_json.log` for
......
......@@ -55,7 +55,7 @@ panels, provide a regular expression in the **Instance label regex** field.
The dashboard uses metrics available in
[Omnibus GitLab](https://docs.gitlab.com/omnibus/) installations.
![GitLab self monitoring default dashboard](img/self_monitoring_default_dashboard.png)
![GitLab self monitoring overview dashboard](img/self_monitoring_overview_dashboard.png)
You can also
[create your own dashboards](../../../operations/metrics/dashboards/index.md).
......@@ -83,7 +83,7 @@ Once the webhook is setup, you can
You can add custom metrics in the self monitoring project by:
1. [Duplicating](../../../operations/metrics/dashboards/index.md#duplicate-a-gitlab-defined-dashboard) the default dashboard.
1. [Duplicating](../../../operations/metrics/dashboards/index.md#duplicate-a-gitlab-defined-dashboard) the overview dashboard.
1. [Editing](../../../operations/metrics/index.md) the newly created dashboard file and configuring it with [dashboard YAML properties](../../../operations/metrics/dashboards/yaml.md).
## Troubleshooting
......
......@@ -251,6 +251,7 @@ are listed in the descriptions of the relevant settings.
| `email_additional_text` | string | no | **(PREMIUM)** Additional text added to the bottom of every email for legal/auditing/compliance reasons |
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `enforce_namespace_storage_limit` | boolean | no | Enabling this permits enforcement of namespace storage limits. |
| `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. |
| `external_auth_client_cert` | string | no | (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service |
| `external_auth_client_key_pass` | string | no | Passphrase to use for the private key when authenticating with the external service this is encrypted when stored |
......
......@@ -46,68 +46,101 @@ https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/
```mermaid
graph TB
HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX]
SSH -- TCP 22 --> GitLabShell[GitLab Shell]
SMTP[SMTP Gateway]
Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX
GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
GitLabShell --> Praefect
Unicorn --> PgBouncer[PgBouncer]
Unicorn --> Redis
Unicorn --> Praefect
Sidekiq --> Redis
Sidekiq --> PgBouncer
Sidekiq --> Praefect
GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis
GitLabWorkhorse --> Praefect
Praefect --> Gitaly
NGINX --> GitLabWorkhorse
NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
NGINX --> Grafana[Grafana]
Grafana -- TCP 9090 --> Prometheus[Prometheus]
Prometheus -- TCP 80, 443 --> Unicorn
RedisExporter[Redis Exporter] --> Redis
Prometheus -- TCP 9121 --> RedisExporter
PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL
PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
Prometheus -- TCP 9187 --> PostgreSQLExporter
Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
Prometheus -- TCP 9168 --> GitLabExporter[GitLab Exporter]
Prometheus -- TCP 9127 --> PgBouncerExporter
GitLabExporter --> PostgreSQL
GitLabExporter --> GitLabShell
GitLabExporter --> Sidekiq
PgBouncer --> Consul
PostgreSQL --> Consul
PgBouncer --> PostgreSQL
NGINX --> Registry
Unicorn --> Registry
NGINX --> Mattermost
Mattermost --- Unicorn
Prometheus --> Alertmanager
Migrations --> PostgreSQL
Runner -- TCP 443 --> NGINX
Unicorn -- TCP 9200 --> Elasticsearch
Sidekiq -- TCP 9200 --> Elasticsearch
Sidekiq -- TCP 80, 443 --> Sentry
Unicorn -- TCP 80, 443 --> Sentry
Sidekiq -- UDP 6831 --> Jaeger
Unicorn -- UDP 6831 --> Jaeger
Gitaly -- UDP 6831 --> Jaeger
GitLabShell -- UDP 6831 --> Jaeger
GitLabWorkhorse -- UDP 6831 --> Jaeger
Alertmanager -- TCP 25 --> SMTP
Sidekiq -- TCP 25 --> SMTP
Unicorn -- TCP 25 --> SMTP
Unicorn -- TCP 369 --> LDAP
Sidekiq -- TCP 369 --> LDAP
Unicorn -- TCP 443 --> ObjectStorage["Object Storage"]
Sidekiq -- TCP 443 --> ObjectStorage
GitLabWorkhorse -- TCP 443 --> ObjectStorage
Registry -- TCP 443 --> ObjectStorage
Geo -- TCP 5432 --> PostgreSQL
HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX]
SSH -- TCP 22 --> GitLabShell[GitLab Shell]
SMTP[SMTP Gateway]
Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX
GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
GitLabShell --> Praefect
Unicorn --> PgBouncer[PgBouncer]
Unicorn --> Redis
Unicorn --> Praefect
Sidekiq --> Redis
Sidekiq --> PgBouncer
Sidekiq --> Praefect
GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis
GitLabWorkhorse --> Praefect
Praefect --> Gitaly
NGINX --> GitLabWorkhorse
NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
NGINX --> Grafana[Grafana]
Grafana -- TCP 9090 --> Prometheus[Prometheus]
Prometheus -- TCP 80, 443 --> Unicorn
RedisExporter[Redis Exporter] --> Redis
Prometheus -- TCP 9121 --> RedisExporter
PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL
PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
Prometheus -- TCP 9187 --> PostgreSQLExporter
Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
Prometheus -- TCP 9168 --> GitLabExporter[GitLab Exporter]
Prometheus -- TCP 9127 --> PgBouncerExporter
GitLabExporter --> PostgreSQL
GitLabExporter --> GitLabShell
GitLabExporter --> Sidekiq
PgBouncer --> Consul
PostgreSQL --> Consul
PgBouncer --> PostgreSQL
NGINX --> Registry
Unicorn --> Registry
NGINX --> Mattermost
Mattermost --- Unicorn
Prometheus --> Alertmanager
Migrations --> PostgreSQL
Runner -- TCP 443 --> NGINX
Unicorn -- TCP 9200 --> Elasticsearch
Sidekiq -- TCP 9200 --> Elasticsearch
Sidekiq -- TCP 80, 443 --> Sentry
Unicorn -- TCP 80, 443 --> Sentry
Sidekiq -- UDP 6831 --> Jaeger
Unicorn -- UDP 6831 --> Jaeger
Gitaly -- UDP 6831 --> Jaeger
GitLabShell -- UDP 6831 --> Jaeger
GitLabWorkhorse -- UDP 6831 --> Jaeger
Alertmanager -- TCP 25 --> SMTP
Sidekiq -- TCP 25 --> SMTP
Unicorn -- TCP 25 --> SMTP
Unicorn -- TCP 369 --> LDAP
Sidekiq -- TCP 369 --> LDAP
Unicorn -- TCP 443 --> ObjectStorage["Object Storage"]
Sidekiq -- TCP 443 --> ObjectStorage
GitLabWorkhorse -- TCP 443 --> ObjectStorage
Registry -- TCP 443 --> ObjectStorage
Geo -- TCP 5432 --> PostgreSQL
click Alertmanager "./architecture.html#alertmanager"
click Praefect "./architecture.html#praefect"
click Geo "./architecture.html#gitlab-geo"
click NGINX "./architecture.html#nginx"
click Runner "./architecture.html#gitlab-runner"
click Registry "./architecture.html#registry"
click ObjectStorage "./architecture.html#minio"
click Mattermost "./architecture.html#mattermost"
click Gitaly "./architecture.html#gitaly"
click Jaeger "./architecture.html#jaeger"
click GitLabWorkhorse "./architecture.html#gitlab-workhorse"
click LDAP "./architecture.html#ldap-authentication"
click Unicorn "./architecture.html#unicorn"
click GitLabShell "./architecture.html#gitlab-shell"
click SSH "./architecture.html#ssh-request-22"
click Sidekiq "./architecture.html#sidekiq"
click Sentry "./architecture.html#sentry"
click GitLabExporter "./architecture.html#gitlab-exporter"
click Elasticsearch "./architecture.html#elasticsearch"
click Migrations "./architecture.html#database-migrations"
click PostgreSQL "./architecture.html#postgresql"
click Consul "./architecture.html#consul"
click PgBouncer "./architecture.html#pgbouncer"
click PgBouncerExporter "./architecture.html#pgbouncer-exporter"
click RedisExporter "./architecture.html#redis-exporter"
click Redis "./architecture.html#redis"
click Prometheus "./architecture.html#prometheus"
click Grafana "./architecture.html#grafana"
click GitLabPages "./architecture.html#gitlab-pages"
click PostgreSQLExporter "./architecture.html#postgresql-exporter"
click SMTP "./architecture.html#outbound-email"
click NodeExporter "./architecture.html#node-exporter"
```
### Component legend
......
......@@ -57,7 +57,7 @@ You can open the link directly into your browser for a
## Embedding metrics in issue templates
You can also embed either the default dashboard metrics or individual metrics in
You can also embed either the overview dashboard metrics or individual metrics in
issue templates. For charts to render side-by-side, separate links to the entire metrics
dashboard or individual metrics by either a comma or a space.
......
---
comments: false
type: index
redirect_to: 'https://docs.gitlab.com'
---
# Books
List of books and resources that may be worth reading.
## Papers
1. **The Humble Programmer**
Edsger W. Dijkstra, 1972 ([paper](https://dl.acm.org/citation.cfm?id=361591))
## Programming
1. **Design Patterns: Elements of Reusable Object-Oriented Software**
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994 ([amazon](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612))
1. **Clean Code: A Handbook of Agile Software Craftsmanship**
Robert C. "Uncle Bob" Martin, 2008 ([amazon](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882))
1. **Code Complete: A Practical Handbook of Software Construction**, 2nd Edition
Steve McConnell, 2004 ([amazon](https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670))
1. **The Pragmatic Programmer: From Journeyman to Master**
Andrew Hunt, David Thomas, 1999 ([amazon](https://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X))
1. **Working Effectively with Legacy Code**
Michael Feathers, 2004 ([amazon](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052))
1. **Eloquent Ruby**
Russ Olsen, 2011 ([amazon](https://www.amazon.com/Eloquent-Ruby-Addison-Wesley-Professional/dp/0321584104))
1. **Domain-Driven Design: Tackling Complexity in the Heart of Software**
Eric Evans, 2003 ([amazon](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215))
1. **How to Solve It: A New Aspect of Mathematical Method**
Polya G. 1957 ([amazon](https://www.amazon.com/How-Solve-Mathematical-Princeton-Science/dp/069116407X))
1. **Software Creativity 2.0**
Robert L. Glass, 2006 ([amazon](https://www.amazon.com/Software-Creativity-2-0-Robert-Glass/dp/0977213315))
1. **Object-Oriented Software Construction**
Bertrand Meyer, 1997 ([amazon](https://www.amazon.com/Object-Oriented-Software-Construction-Book-CD-ROM/dp/0136291554))
1. **Refactoring: Improving the Design of Existing Code**
Martin Fowler, Kent Beck, 1999 ([amazon](https://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672))
1. **Test Driven Development: By Example**
Kent Beck, 2002 ([amazon](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530))
1. **Algorithms in C++: Fundamentals, Data Structure, Sorting, Searching**
Robert Sedgewick, 1990 ([amazon](https://www.amazon.com/Algorithms-Parts-1-4-Fundamentals-Structure/dp/0201350882))
1. **Effective C++**
Scott Mayers, 1996 ([amazon](https://www.amazon.com/Effective-Specific-Improve-Programs-Designs/dp/0321334876))
1. **Extreme Programming Explained: Embrace Change**
Kent Beck, 1999 ([amazon](https://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658))
1. **The Art of Computer Programming**
Donald E. Knuth, 1997 ([amazon](https://www.amazon.com/Computer-Programming-Volumes-1-4A-Boxed/dp/0321751043))
1. **Writing Efficient Programs**
Jon Louis Bentley, 1982 ([amazon](https://www.amazon.com/Writing-Efficient-Programs-Prentice-Hall-Software/dp/013970244X))
1. **The Mythical Man-Month: Essays on Software Engineering**
Frederick Phillips Brooks, 1975 ([amazon](https://www.amazon.com/Mythical-Man-Month-Essays-Software-Engineering/dp/0201006502))
1. **Peopleware: Productive Projects and Teams** 3rd Edition
Tom DeMarco, Tim Lister, 2013 ([amazon](https://www.amazon.com/Peopleware-Productive-Projects-Teams-3rd/dp/0321934113))
1. **Principles Of Software Engineering Management**
Tom Gilb, 1988 ([amazon](https://www.amazon.com/Principles-Software-Engineering-Management-Gilb/dp/0201192462))
## Other
1. **Thinking, Fast and Slow**
Daniel Kahneman, 2013 ([amazon](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555))
1. **The Social Animal** 11th Edition
Elliot Aronson, 2011 ([amazon](https://www.amazon.com/Social-Animal-Elliot-Aronson/dp/1429233419))
1. **Influence: Science and Practice** 5th Edition
Robert B. Cialdini, 2008 ([amazon](https://www.amazon.com/Influence-Practice-Robert-B-Cialdini/dp/0205609996))
1. **Getting to Yes: Negotiating Agreement Without Giving In**
Roger Fisher, William L. Ury, Bruce Patton, 2011 ([amazon](https://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757))
1. **How to Win Friends & Influence People**
Dale Carnegie, 1981 ([amazon](https://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034))
Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
---
comments: false
type: index
redirect_to: 'https://docs.gitlab.com'
---
# The GitLab Book Club
The Book Club is a casual meet-up to read and discuss books we like.
We'll find a time that suits most, if not all.
See the [book list](booklist.md) for additional recommendations.
## Currently reading : Books about remote work
1. **Remote: Office not required**
David Heinemeier Hansson and Jason Fried, 2013
([Amazon](https://www.amazon.co.uk/dp/0091954673/ref=cm_sw_r_tw_dp_x_0yy9EbZ2WXJ6Y))
1. **The Year Without Pants**
Scott Berkun, 2013 ([ScottBerkun.com](https://scottberkun.com/yearwithoutpants/))
Any other books you'd like to suggest? Edit this page and add them to the queue.
Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
---
comments: false
redirect_to: 'https://docs.gitlab.com'
---
# Glossary
This page has been removed after an effort to ensure that all applicable GitLab-specific
terms are available in context on the relevant [GitLab Documentation](https://docs.gitlab.com/)
or <https://about.gitlab.com/> pages.
If you are looking for a definition of a specific term, please search these sites.
Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
......@@ -217,6 +217,10 @@ As a workaround, try an alternate mapping:
#### How do I diagnose why a user is unable to sign in
Ensure that the user has been added to the SCIM app.
If you receive "User is not linked to a SAML account", then most likely the user already exists in GitLab. Have the user follow the [User access and linking setup](#user-access-and-linking-setup) instructions.
The **Identity** (`extern_uid`) value stored by GitLab is updated by SCIM whenever `id` or `externalId` changes. Users won't be able to sign in unless the GitLab Identity (`extern_uid`) value matches the `NameId` sent by SAML.
This value is also used by SCIM to match users on the `id`, and is updated by SCIM whenever the `id` or `externalId` values change.
......
......@@ -22,7 +22,7 @@ module Gitlab
return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql])
current_transaction.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do
buckets [0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
buckets [0.05, 0.1]
end
increment_db_counters(payload)
......
......@@ -12,8 +12,6 @@
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab
class UsageData
BATCH_SIZE = 100
class << self
include Gitlab::Utils::UsageData
include Gitlab::Utils::StrongMemoize
......@@ -353,29 +351,25 @@ module Gitlab
results
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
# rubocop: disable UsageData/LargeTable:
Service.available_services_names.without('jira').each_with_object({}) do |service_name, response|
Service.available_services_names.each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, type: "#{service_name}_service".camelize))
end.merge(jira_usage).merge(jira_import_usage)
end.merge(jira_usage, jira_import_usage)
# rubocop: enable UsageData/LargeTable:
end
def jira_usage
# Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
# so we can just check for subdomains of atlassian.net
results = {
projects_jira_server_active: 0,
projects_jira_cloud_active: 0,
projects_jira_active: 0
projects_jira_cloud_active: 0
}
# rubocop: disable UsageData/LargeTable:
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: BATCH_SIZE) do |services|
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: 100) do |services|
counts = services.group_by do |service|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
service_url = service.data_fields&.url || (service.properties && service.properties['url'])
......@@ -384,22 +378,12 @@ module Gitlab
results[:projects_jira_server_active] += counts[:server].size if counts[:server]
results[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud]
results[:projects_jira_active] += services.size
end
# rubocop: enable UsageData/LargeTable:
results
rescue ActiveRecord::StatementInvalid
{ projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK }
end
# rubocop: disable UsageData/LargeTable
def successful_deployments_with_cluster(scope)
scope
.joins(cluster: :deployments)
.merge(Clusters::Cluster.enabled)
.merge(Deployment.success)
{ projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK }
end
# rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord
def jira_import_usage
......@@ -414,6 +398,17 @@ module Gitlab
# rubocop: enable UsageData/LargeTable
end
# rubocop: disable CodeReuse/ActiveRecord
# rubocop: disable UsageData/LargeTable
def successful_deployments_with_cluster(scope)
scope
.joins(cluster: :deployments)
.merge(Clusters::Cluster.enabled)
.merge(Deployment.success)
end
# rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord
def user_preferences_usage
{} # augmented in EE
end
......
......@@ -7603,9 +7603,6 @@ msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default dashboard"
msgstr ""
msgid "Default deletion adjourned period"
msgstr ""
......
......@@ -28,7 +28,6 @@ Migration/UpdateLargeTable:
- :resource_label_events
- :routes
- :sent_notifications
- :services
- :system_note_metadata
- :taggings
- :todos
......
......@@ -165,7 +165,7 @@ RSpec.describe MetricsDashboard do
it 'adds starred dashboard information and sorts the list' do
all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') }
expected_response = [
{ "display_name" => "Default dashboard", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/common_metrics.yml' }) },
{ "display_name" => "Overview", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/common_metrics.yml' }) },
{ "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/anomaly.yml' }) },
{ "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/errors.yml' }) },
{ "display_name" => "test.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/test.yml' }) }
......
......@@ -2,7 +2,17 @@
require 'spec_helper'
RSpec.shared_examples_for 'snippet editor' do
RSpec.describe 'User creates snippet', :js do
include DropzoneHelper
let_it_be(:user) { create(:user) }
let(:title) { 'My Snippet Title' }
let(:file_content) { 'Hello World!' }
let(:md_description) { 'My Snippet **Description**' }
let(:description) { 'My Snippet Description' }
let(:created_snippet) { Snippet.last }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
......@@ -14,15 +24,15 @@ RSpec.shared_examples_for 'snippet editor' do
end
def fill_form
fill_in 'personal_snippet_title', with: 'My Snippet Title'
fill_in 'personal_snippet_title', with: title
# Click placeholder first to expand full description field
description_field.click
fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
fill_in 'personal_snippet_description', with: md_description
page.within('.file-editor') do
el = find('.inputarea')
el.send_keys 'Hello World!'
el.send_keys file_content
end
end
......@@ -34,12 +44,12 @@ RSpec.shared_examples_for 'snippet editor' do
click_button('Create snippet')
wait_for_requests
expect(page).to have_content('My Snippet Title')
expect(page).to have_content(title)
page.within('.snippet-header .description') do
expect(page).to have_content('My Snippet Description')
expect(page).to have_content(description)
expect(page).to have_selector('strong')
end
expect(page).to have_content('Hello World!')
expect(page).to have_content(file_content)
end
it 'previews a snippet with file' do
......@@ -57,7 +67,7 @@ RSpec.shared_examples_for 'snippet editor' do
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
# Adds a cache buster for checking if the image exists as Selenium is now handling the cached regquests
# Adds a cache buster for checking if the image exists as Selenium is now handling the cached requests
# not anymore as requests when they come straight from memory cache.
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
......@@ -99,15 +109,10 @@ RSpec.shared_examples_for 'snippet editor' do
wait_for_requests
end
it 'displays the error' do
it 'renders the new page and displays the error' do
expect(page).to have_content(error)
end
it 'renders new page' do
expect(page).to have_content('New Snippet')
end
it 'has the correct action path' do
action = find('form.snippet-form')['action']
expect(action).to match(%r{/snippets\z})
end
......@@ -116,46 +121,10 @@ RSpec.shared_examples_for 'snippet editor' do
it 'validation fails for the first time' do
visit new_snippet_path
fill_in 'personal_snippet_title', with: 'My Snippet Title'
fill_in 'personal_snippet_title', with: title
click_button('Create snippet')
expect(page).to have_selector('#error_explanation')
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
click_button('Create snippet')
wait_for_requests
expect(page).to have_content('My Snippet Title')
page.within('.snippet-header .description') do
expect(page).to have_content('My Snippet Description')
expect(page).to have_selector('strong')
end
expect(page).to have_content('Hello World!')
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
it 'Authenticated user creates a snippet with + in filename' do
visit new_snippet_path
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
el = find('.inputarea')
el.send_keys 'Hello World!'
end
click_button 'Create snippet'
wait_for_requests
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('snippet+file+name')
expect(page).to have_content('Hello World!')
end
context 'when snippets default visibility level is restricted' do
......@@ -172,20 +141,7 @@ RSpec.shared_examples_for 'snippet editor' do
click_button('Create snippet')
wait_for_requests
visit snippets_path
click_link('Internal')
expect(page).to have_content('My Snippet Title')
created_snippet = Snippet.last
expect(created_snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
end
RSpec.describe 'User creates snippet', :js do
include DropzoneHelper
let_it_be(:user) { create(:user) }
it_behaves_like "snippet editor"
end
......@@ -72,6 +72,7 @@ describe('Design management index page', () => {
const dropzoneClasses = () => findDropzone().classes();
const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
function createComponent({
loading = false,
......@@ -508,6 +509,10 @@ describe('Design management index page', () => {
});
event = new Event('paste');
event.clipboardData = {
files: [{ name: 'image.png', type: 'image/png' }],
getData: () => 'test.png',
};
router.replace({
name: DESIGNS_ROUTE_NAME,
......@@ -517,43 +522,52 @@ describe('Design management index page', () => {
});
});
it('calls onUploadDesign with valid paste', () => {
event.clipboardData = {
files: [{ name: 'image.png', type: 'image/png' }],
getData: () => 'test.png',
};
it('does not call paste event if designs wrapper is not hovered', () => {
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
new File([{ name: 'image.png' }], 'test.png'),
]);
expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
});
it('renames a design if it has an image.png filename', () => {
event.clipboardData = {
files: [{ name: 'image.png', type: 'image/png' }],
getData: () => 'image.png',
};
describe('when designs wrapper is hovered', () => {
beforeEach(() => {
findDesignsWrapper().trigger('mouseenter');
});
document.dispatchEvent(event);
it('calls onUploadDesign with valid paste', () => {
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
]);
});
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
new File([{ name: 'image.png' }], 'test.png'),
]);
});
it('does not call onUploadDesign with invalid paste', () => {
event.clipboardData = {
items: [{ type: 'text/plain' }, { type: 'text' }],
files: [],
};
it('renames a design if it has an image.png filename', () => {
document.dispatchEvent(event);
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
]);
});
expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
it('does not call onUploadDesign with invalid paste', () => {
event.clipboardData = {
items: [{ type: 'text/plain' }, { type: 'text' }],
files: [],
};
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
});
it('removes onPaste listener after mouseleave event', async () => {
findDesignsWrapper().trigger('mouseleave');
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
});
});
});
......
......@@ -257,7 +257,7 @@ describe('Dashboard header', () => {
});
const duplicableCases = [
null, // When no path is specified, it uses the default dashboard path.
null, // When no path is specified, it uses the overview dashboard path.
dashboardGitResponse[0].path,
dashboardGitResponse[2].path,
selfMonitoringDashboardGitResponse[0].path,
......
......@@ -886,7 +886,7 @@ describe('Dashboard', () => {
return wrapper.vm.$nextTick();
});
it('is not present for the default dashboard', () => {
it('is not present for the overview dashboard', () => {
expect(findEditLink().exists()).toBe(false);
});
......@@ -905,7 +905,7 @@ describe('Dashboard', () => {
describe('document title', () => {
const originalTitle = 'Original Title';
const defaultDashboardName = dashboardGitResponse[0].display_name;
const overviewDashboardName = dashboardGitResponse[0].display_name;
beforeEach(() => {
document.title = originalTitle;
......@@ -916,11 +916,11 @@ describe('Dashboard', () => {
document.title = '';
});
it('is prepended with default dashboard name by default', () => {
it('is prepended with the overview dashboard name by default', () => {
setupAllDashboards(store);
return wrapper.vm.$nextTick().then(() => {
expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
......@@ -935,11 +935,11 @@ describe('Dashboard', () => {
});
});
it('is prepended with default dashboard name is path is not known', () => {
it('is prepended with the overview dashboard name if path is not known', () => {
setupAllDashboards(store, 'unknown/path');
return wrapper.vm.$nextTick().then(() => {
expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
......
......@@ -73,7 +73,7 @@ describe('DashboardsDropdown', () => {
});
it('filters dropdown items when searched for item exists in the list', () => {
const searchTerm = 'Default';
const searchTerm = 'Overview';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick().then(() => {
......
......@@ -170,7 +170,7 @@ export const environmentData = [
export const dashboardGitResponse = [
{
default: true,
display_name: 'Default',
display_name: 'Overview',
can_edit: false,
system_dashboard: true,
out_of_the_box_dashboard: true,
......
......@@ -380,7 +380,7 @@ describe('Monitoring store Getters', () => {
);
});
it('returns a non-default dashboard', () => {
it('returns a dashboard different from the overview dashboard', () => {
const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path,
......@@ -391,7 +391,7 @@ describe('Monitoring store Getters', () => {
);
});
it('returns a default dashboard when no dashboard is selected', () => {
it('returns the overview dashboard when no dashboard is selected', () => {
const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: null,
......@@ -402,7 +402,7 @@ describe('Monitoring store Getters', () => {
);
});
it('returns a default dashboard when dashboard cannot be found', () => {
it('returns the overview dashboard when dashboard cannot be found', () => {
const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path',
......
......@@ -34,6 +34,7 @@ describe('Release edit/new component', () => {
getters = {
isValid: () => true,
isExistingRelease: () => true,
validationErrors: () => ({
assets: {
links: [],
......@@ -96,28 +97,6 @@ describe('Release edit/new component', () => {
);
});
it('renders the correct tag name in the "Tag name" field', () => {
expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
});
it('renders the correct help text under the "Tag name" field', () => {
const helperText = wrapper.find('#tag-name-help');
const helperTextLink = helperText.find('a');
const helperTextLinkAttrs = helperTextLink.attributes();
expect(helperText.text()).toBe(
'Changing a Release tag is only supported via Releases API. More information',
);
expect(helperTextLink.text()).toBe('More information');
expect(helperTextLinkAttrs).toEqual(
expect.objectContaining({
href: state.updateReleaseApiDocsPath,
rel: 'noopener noreferrer',
target: '_blank',
}),
);
});
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(release.name);
});
......
import { GlFormInput } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_DOCS_PATH = '/help/test/docs/path';
describe('releases/components/tag_field_existing', () => {
let store;
let wrapper;
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldExisting, {
store,
});
};
const findInput = () => wrapper.find(GlFormInput);
const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
const findHelpLink = () => {
const link = findHelp().find('a');
return {
text: link.text(),
href: link.attributes('href'),
target: link.attributes('target'),
};
};
beforeEach(() => {
store = createStore({
modules: {
detail: createDetailModule({
updateReleaseApiDocsPath: TEST_DOCS_PATH,
tagName: TEST_TAG_NAME,
}),
},
});
store.state.detail.release = {
tagName: TEST_TAG_NAME,
};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('default', () => {
it('shows the tag name', () => {
createComponent();
expect(findInput().attributes()).toMatchObject({
disabled: '',
value: TEST_TAG_NAME,
});
});
it('shows help', () => {
createComponent(mount);
expect(findHelp().text()).toMatchInterpolatedText(
'Changing a Release tag is only supported via Releases API. More information',
);
const helpLink = findHelpLink();
expect(helpLink).toEqual({
text: 'More information',
href: TEST_DOCS_PATH,
target: '_blank',
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
describe('releases/components/tag_field_new', () => {
let store;
let wrapper;
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldNew, {
store,
});
};
beforeEach(() => {
store = createStore({
modules: {
detail: createDetailModule({}),
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders a placeholder component', () => {
createComponent();
expect(wrapper.exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import TagField from '~/releases/components/tag_field.vue';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
describe('releases/components/tag_field', () => {
let store;
let wrapper;
const createComponent = ({ originalRelease }) => {
store = createStore({
modules: {
detail: createDetailModule({}),
},
});
store.state.detail.originalRelease = originalRelease;
wrapper = shallowMount(TagField, { store });
};
const findTagFieldNew = () => wrapper.find(TagFieldNew);
const findTagFieldExisting = () => wrapper.find(TagFieldExisting);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when an existing release is being edited', () => {
beforeEach(() => {
const originalRelease = { name: 'Version 1.0' };
createComponent({ originalRelease });
});
it('renders the TagFieldExisting component', () => {
expect(findTagFieldExisting().exists()).toBe(true);
});
it('does not render the TagFieldNew component', () => {
expect(findTagFieldNew().exists()).toBe(false);
});
});
describe('when a new release is being created', () => {
beforeEach(() => {
createComponent({ originalRelease: null });
});
it('renders the TagFieldNew component', () => {
expect(findTagFieldNew().exists()).toBe(true);
});
it('does not render the TagFieldExisting component', () => {
expect(findTagFieldExisting().exists()).toBe(false);
});
});
});
import * as getters from '~/releases/stores/modules/detail/getters';
describe('Release detail getters', () => {
describe('isExistingRelease', () => {
it('returns true if the release is an existing release that already exists in the database', () => {
const state = { originalRelease: { name: 'The first release' } };
expect(getters.isExistingRelease(state)).toBe(true);
});
it('returns false if the release is a new release that has not yet been saved to the database', () => {
const state = { originalRelease: null };
expect(getters.isExistingRelease(state)).toBe(false);
});
});
describe('releaseLinksToCreate', () => {
it("returns an empty array if state.release doesn't exist", () => {
const state = {};
......
......@@ -142,7 +142,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
describe '.find_all_paths' do
let(:all_dashboard_paths) { described_class.find_all_paths(project) }
let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Default dashboard', default: true, system_dashboard: true, out_of_the_box_dashboard: true } }
let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Overview', default: true, system_dashboard: true, out_of_the_box_dashboard: true } }
it 'includes only the system dashboard by default' do
expect(all_dashboard_paths).to eq([system_dashboard])
......@@ -163,7 +163,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
let(:self_monitoring_dashboard) do
{
path: self_monitoring_dashboard_path,
display_name: 'Default dashboard',
display_name: 'Overview',
default: true,
system_dashboard: true,
out_of_the_box_dashboard: true
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CounterAttribute, :counter_attribute, :clean_gitlab_redis_shared_state do
using RSpec::Parameterized::TableSyntax
let(:project_statistics) { create(:project_statistics) }
let(:model) { CounterAttributeModel.find(project_statistics.id) }
it_behaves_like CounterAttribute, [:build_artifacts_size, :commit_count] do
let(:model) { CounterAttributeModel.find(project_statistics.id) }
end
describe '.steal_increments' do
let(:increment_key) { 'counters:Model:123:attribute' }
let(:flushed_key) { 'counter:Model:123:attribute:flushed' }
subject { model.send(:steal_increments, increment_key, flushed_key) }
where(:increment, :flushed, :result, :flushed_key_present) do
nil | nil | 0 | false
nil | 0 | 0 | false
0 | 0 | 0 | false
1 | 0 | 1 | true
1 | nil | 1 | true
1 | 1 | 2 | true
1 | -2 | -1 | true
-1 | 1 | 0 | false
end
with_them do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set(increment_key, increment) if increment
redis.set(flushed_key, flushed) if flushed
end
end
it { is_expected.to eq(result) }
it 'drops the increment key and creates the flushed key if it does not exist' do
subject
Gitlab::Redis::SharedState.with do |redis|
expect(redis.exists(increment_key)).to be_falsey
expect(redis.exists(flushed_key)).to eq(flushed_key_present)
end
end
end
end
end
......@@ -328,8 +328,8 @@ RSpec.describe ProjectStatistics do
it 'increases also storage size by that amount' do
expect { described_class.increment_statistic(project.id, stat, 20) }
.to change { statistics.reload.storage_size }
.by(20)
.to change { statistics.reload.storage_size }
.by(20)
end
end
......
......@@ -132,7 +132,7 @@ RSpec.describe Metrics::Dashboard::DynamicEmbedService, :use_clean_rails_memory_
end
shared_examples 'uses system dashboard' do
it 'uses the default dashboard' do
it 'uses the overview dashboard' do
expect(Gitlab::Metrics::Dashboard::Finder)
.to receive(:find_raw)
.with(project, dashboard_path: system_dashboard_path)
......
# frozen_string_literal: true
RSpec.configure do |config|
config.before(:each, :counter_attribute) do
stub_const('CounterAttributeModel', Class.new(ProjectStatistics))
CounterAttributeModel.class_eval do
include CounterAttribute
counter_attribute :build_artifacts_size
counter_attribute :commit_count
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples_for CounterAttribute do |counter_attributes|
it 'defines a Redis counter_key' do
expect(model.counter_key(:counter_name))
.to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name")
end
it 'defines a method to store counters' do
expect(model.class.counter_attributes.to_a).to eq(counter_attributes)
end
counter_attributes.each do |attribute|
describe attribute do
describe '#delayed_increment_counter', :redis do
let(:increment) { 10 }
subject { model.delayed_increment_counter(attribute, increment) }
context 'when attribute is a counter attribute' do
where(:increment) { [10, -3] }
with_them do
it 'increments the counter in Redis' do
subject
Gitlab::Redis::SharedState.with do |redis|
counter = redis.get(model.counter_key(attribute))
expect(counter).to eq(increment.to_s)
end
end
it 'does not increment the counter for the record' do
expect { subject }.not_to change { model.reset.read_attribute(attribute) }
end
it 'schedules a worker to flush counter increments asynchronously' do
expect(FlushCounterIncrementsWorker).to receive(:perform_in)
.with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute)
.and_call_original
subject
end
end
context 'when increment is 0' do
let(:increment) { 0 }
it 'does nothing' do
expect(FlushCounterIncrementsWorker).not_to receive(:perform_in)
expect(model).not_to receive(:update!)
subject
end
end
end
context 'when attribute is not a counter attribute' do
it 'delegates to ActiveRecord update!' do
expect { model.delayed_increment_counter(:unknown_attribute, 10) }
.to raise_error(ActiveModel::MissingAttributeError)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(efficient_counter_attribute: false)
end
it 'delegates to ActiveRecord update!' do
expect { subject }
.to change { model.reset.read_attribute(attribute) }.by(increment)
end
it 'does not increment the counter in Redis' do
subject
Gitlab::Redis::SharedState.with do |redis|
counter = redis.get(model.counter_key(attribute))
expect(counter).to be_nil
end
end
end
end
end
end
describe '.flush_increments_to_database!', :redis do
let(:incremented_attribute) { counter_attributes.first }
subject { model.flush_increments_to_database!(incremented_attribute) }
it 'obtains an exclusive lease during processing' do
expect(model)
.to receive(:in_lock)
.with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
.and_call_original
subject
end
context 'when there is a counter to flush' do
before do
model.delayed_increment_counter(incremented_attribute, 10)
model.delayed_increment_counter(incremented_attribute, -3)
end
it 'updates the record' do
expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7)
end
it 'removes the increment entry from Redis' do
Gitlab::Redis::SharedState.with do |redis|
key_exists = redis.exists(model.counter_key(incremented_attribute))
expect(key_exists).to be_truthy
end
subject
Gitlab::Redis::SharedState.with do |redis|
key_exists = redis.exists(model.counter_key(incremented_attribute))
expect(key_exists).to be_falsey
end
end
end
context 'when there are no counters to flush' do
context 'when there are no counters in the relative :flushed key' do
it 'does not change the record' do
expect { subject }.not_to change { model.reset.attributes }
end
end
# This can be the case where updating counters in the database fails with error
# and retrying the worker will retry flushing the counters but the main key has
# disappeared and the increment has been moved to the "<...>:flushed" key.
context 'when there are counters in the relative :flushed key' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
end
end
it 'updates the record' do
expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10)
end
it 'deletes the relative :flushed key' do
subject
Gitlab::Redis::SharedState.with do |redis|
key_exists = redis.exists(model.counter_flushed_key(incremented_attribute))
expect(key_exists).to be_falsey
end
end
end
end
context 'when deleting :flushed key fails' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
expect(redis).to receive(:del).and_raise('could not delete key')
end
end
it 'does a rollback of the counter update' do
expect { subject }.to raise_error('could not delete key')
expect(model.reset.read_attribute(incremented_attribute)).to eq(0)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe FlushCounterIncrementsWorker, :counter_attribute do
let(:project_statistics) { create(:project_statistics) }
let(:model) { CounterAttributeModel.find(project_statistics.id) }
describe '#perform', :redis do
let(:attribute) { model.class.counter_attributes.first }
let(:worker) { described_class.new }
subject { worker.perform(model.class.name, model.id, attribute) }
it 'flushes increments to database' do
expect(model.class).to receive(:find_by_id).and_return(model)
expect(model)
.to receive(:flush_increments_to_database!)
.with(attribute)
.and_call_original
subject
end
context 'when model class does not exist' do
subject { worker.perform('non-existend-model') }
it 'does nothing' do
expect(worker).not_to receive(:in_lock)
end
end
context 'when record does not exist' do
subject { worker.perform(model.class.name, model.id + 100, attribute) }
it 'does nothing' do
expect(worker).not_to receive(:in_lock)
end
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册