diff options
211 files changed, 2642 insertions, 755 deletions
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 8a49715e0e8..2ba6b68a53e 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -26,7 +26,7 @@ Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/de ### Testing -<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/ --> +<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further help: https://about.gitlab.com/handbook/engineering/quality/test-engineering/ --> ### What does success look like, and how can we measure that? diff --git a/.gitlab/issue_templates/Test plan.md b/.gitlab/issue_templates/Test plan.md index 3aedd5859d3..f194adebc87 100644 --- a/.gitlab/issue_templates/Test plan.md +++ b/.gitlab/issue_templates/Test plan.md @@ -89,7 +89,7 @@ New end-to-end and integration tests (Selenium and API) should be added to the Please note if automated tests already exist. -When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ce/development/testing_guide/testing_levels.html) +When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html) in mind. --> diff --git a/.rubocop.yml b/.rubocop.yml index 79e06439ac2..b75c63e1f58 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.6 TargetRailsVersion: 5.0 Exclude: - 'vendor/**/*' @@ -51,6 +51,7 @@ gem 'jwt', '~> 2.1.0' # Spam and anti-bot protection gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' +gem 'invisible_captcha', '~> 0.12.1' # Two-factor authentication gem 'devise-two-factor', '~> 3.0.0' @@ -297,6 +298,9 @@ gem 'batch-loader', '~> 1.4.0' # Perf bar gem 'peek', '~> 1.0.1' +# Snowplow events tracking +gem 'snowplow-tracker', '~> 0.6.1' + # Memory benchmarks gem 'derailed_benchmarks', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6aa96d54abb..16d7f63cb66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -152,6 +152,7 @@ GEM concurrent-ruby-ext (1.1.5) concurrent-ruby (= 1.1.5) connection_pool (2.2.2) + contracts (0.11.0) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) @@ -437,6 +438,8 @@ GEM influxdb (0.2.3) cause json + invisible_captcha (0.12.1) + rails (>= 3.2.0) ipaddress (0.8.3) jaeger-client (0.10.0) opentracing (~> 0.3) @@ -901,6 +904,8 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) slack-notifier (1.5.1) + snowplow-tracker (0.6.1) + contracts (~> 0.7, <= 0.11) spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) @@ -1126,6 +1131,7 @@ DEPENDENCIES httparty (~> 0.16.4) icalendar influxdb (~> 0.2) + invisible_captcha (~> 0.12.1) jira-ruby (~> 1.4) js_regex (~> 3.1) json-schema (~> 2.8.0) @@ -1229,6 +1235,7 @@ DEPENDENCIES simple_po_parser (~> 1.1.2) simplecov (~> 0.16.1) slack-notifier (~> 1.5.1) + snowplow-tracker (~> 0.6.1) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) sprockets (~> 3.7.0) @@ -1,4 +1,12 @@ -Copyright GitLab B.V. +Copyright (c) 2011-present GitLab B.V. + +Portions of this software are licensed as follows: + +* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE". +* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 73bfcc988cc..054e2d02461 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ To see how GitLab looks please see the [features page on our website](https://ab - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Completely free and open source (MIT Expat license) -## Hiring - -We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). - ## Editions There are two editions of GitLab: @@ -31,6 +27,15 @@ There are two editions of GitLab: - GitLab Community Edition (CE) is available freely under the MIT Expat license. - GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). +## Licensing + +See the [LICENSE](LICENSE) file for licensing information as it pertains to +files in this repository. + +## Hiring + +We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). + ## Website On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: @@ -58,14 +63,6 @@ There are various other options to install GitLab, please refer to the [installa GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details. -## Licensing - -GitLab Community Edition (CE) is available freely under the MIT Expat license. - -All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. - -All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. - ## Install a development environment To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9e97f345717..ba33d72b1f3 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -107,6 +107,7 @@ function deferredInitialisation() { .then(() => { $('select.select2').select2({ width: 'resolve', + minimumResultsForSearch: 10, dropdownAutoWidth: true, }); diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 5b950f8c966..838447e6c75 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -1,7 +1,6 @@ <script> import { __ } from '~/locale'; -import { mapState } from 'vuex'; -import { GlLink, GlButton } from '@gitlab/ui'; +import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; @@ -16,7 +15,6 @@ let debouncedResize; export default { components: { GlAreaChart, - GlButton, GlChartSeriesLabel, GlLink, Icon, @@ -69,7 +67,6 @@ export default { }; }, computed: { - ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), chartData() { // Transforms & supplements query data to render appropriate labels & styles // Input: [{ queryAttributes1 }, { queryAttributes2 }] @@ -179,18 +176,6 @@ export default { yAxisLabel() { return `${this.graphData.y_label}`; }, - csvText() { - const chartData = this.chartData[0].data; - const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings - return chartData.reduce((csv, data) => { - const row = data.join(','); - return `${csv}${row}\r\n`; - }, header); - }, - downloadLink() { - const data = new Blob([this.csvText], { type: 'text/plain' }); - return window.URL.createObjectURL(data); - }, }, watch: { containerWidth: 'onResize', @@ -259,16 +244,6 @@ export default { <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> - <gl-button - v-if="exportMetricsToCsvEnabled" - :href="downloadLink" - :title="__('Download CSV')" - :aria-label="__('Download CSV')" - style="margin-left: 200px;" - download="chart_metrics.csv" - > - {{ __('Download CSV') }} - </gl-button> <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 782e4310f3e..587392adbc3 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -235,6 +235,19 @@ export default { chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), ); }, + csvText(graphData) { + const chartData = graphData.queries[0].result[0].values; + const yLabel = graphData.y_label; + const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadCsv(graphData) { + const data = new Blob([this.csvText(graphData)], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed // Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845 getGraphAlerts(queries) { @@ -448,7 +461,6 @@ export default { @setAlerts="setAlerts" /> <gl-dropdown - v-if="alertWidgetAvailable" v-gl-tooltip class="mx-2" toggle-class="btn btn-transparent border-0" @@ -459,6 +471,9 @@ export default { <template slot="button-content"> <icon name="ellipsis_v" class="text-secondary" /> </template> + <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv"> + {{ __('Download CSV') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}-${graphIndex}`" diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 295c0851f12..3fbac71f3d7 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -1,7 +1,14 @@ <script> import { mapState } from 'vuex'; import _ from 'underscore'; -import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; +import { + GlDropdown, + GlDropdownItem, + GlModal, + GlModalDirective, + GlTooltipDirective, +} from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; import MonitorAreaChart from './charts/area.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorEmptyChart from './charts/empty_chart.vue'; @@ -11,12 +18,14 @@ export default { MonitorAreaChart, MonitorSingleStatChart, MonitorEmptyChart, + Icon, GlDropdown, GlDropdownItem, GlModal, }, directives: { GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, }, props: { graphData: { @@ -41,6 +50,19 @@ export default { graphDataHasMetrics() { return this.graphData.queries[0].result.length > 0; }, + csvText() { + const chartData = this.graphData.queries[0].result[0].values; + const yLabel = this.graphData.y_label; + const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadCsv() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, }, methods: { getGraphAlerts(queries) { @@ -81,7 +103,6 @@ export default { @setAlerts="setAlerts" /> <gl-dropdown - v-if="alertWidgetAvailable" v-gl-tooltip class="mx-2" toggle-class="btn btn-transparent border-0" @@ -92,6 +113,9 @@ export default { <template slot="button-content"> <icon name="ellipsis_v" class="text-secondary" /> </template> + <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv"> + {{ __('Download CSV') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`"> {{ __('Alerts') }} </gl-dropdown-item> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 366034becd0..c0fee1ebb99 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -13,7 +13,6 @@ export default (props = {}) => { prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, - exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled, }); } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index a9c491c7c6c..0cbad179f17 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => { export const setFeatureFlags = ( { commit }, - { - prometheusEndpointEnabled, - multipleDashboardsEnabled, - additionalPanelTypesEnabled, - exportMetricsToCsvEnabled, - }, + { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, ) => { commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); - commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled); }; export const setShowErrorBanner = ({ commit }, enabled) => { diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 9ec8214b167..4b1aadbcf05 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; -export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index a2dceb21fc0..b19520d6638 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -99,7 +99,4 @@ export default { [types.SET_SHOW_ERROR_BANNER](state, enabled) { state.showErrorBanner = enabled; }, - [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) { - state.exportMetricsToCsvEnabled = enabled; - }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index a14a25e3a20..440bdc951e0 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -10,7 +10,6 @@ export default () => ({ useDashboardEndpoint: false, multipleDashboardsEnabled: false, additionalPanelTypesEnabled: false, - exportMetricsToCsvEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, showErrorBanner: true, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index d4514767912..e294e1de976 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -94,9 +94,6 @@ export default { return __('Merge'); }, - shouldShowMergeOptionsDropdown() { - return this.isAutoMergeAvailable && !this.mr.onlyAllowMergeIfPipelineSucceeds; - }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; }, @@ -246,7 +243,7 @@ export default { {{ mergeButtonText }} </button> <button - v-if="isAutoMergeAvailable" + v-if="shouldShowMergeImmediatelyDropdown" :disabled="isMergeButtonDisabled" type="button" class="btn btn-sm btn-info dropdown-toggle js-merge-moment" @@ -256,7 +253,7 @@ export default { <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> </button> <ul - v-if="shouldShowMergeOptionsDropdown" + v-if="shouldShowMergeImmediatelyDropdown" class="dropdown-menu dropdown-menu-right" role="menu" > diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index 116d537c463..eef49e20159 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -15,5 +15,8 @@ export default { // MWPS is currently the only auto merge strategy available in CE return __('Merge when pipeline succeeds'); }, + shouldShowMergeImmediatelyDropdown() { + return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds; + }, }, }; diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 643b20c56bc..c5bb2a1256a 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -9,7 +9,9 @@ .bs-callout { margin: $gl-padding 0; padding: $gl-padding; - border-left: 3px solid $border-color; + border-color: $border-color; + border-style: solid; + border-width: 0 0 0 3px; color: $text-color; background: $gray-light; @@ -48,6 +50,10 @@ background-color: $blue-100; border-color: $blue-200; color: $blue-700; + + h4 { + color: $blue-700; + } } .bs-callout-success { diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 60400f10ca5..379df1c4db1 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -1,19 +1,3 @@ -.new-wiki-page { - .new-wiki-page-slug-tip { - display: inline-block; - max-width: 100%; - margin-top: 5px; - } -} - -.wiki-form { - .edit-wiki-page-slug-tip { - display: inline-block; - max-width: 100%; - margin-top: 5px; - } -} - .title .edit-wiki-header { width: 780px; margin-left: auto; @@ -22,7 +6,6 @@ } .wiki-page-header { - @extend .top-area; position: relative; .wiki-breadcrumb { diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb new file mode 100644 index 00000000000..c9f66e5c194 --- /dev/null +++ b/app/controllers/concerns/invisible_captcha.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module InvisibleCaptcha + extend ActiveSupport::Concern + + included do + invisible_captcha only: :create, on_spam: :on_honeypot_spam_callback, on_timestamp_spam: :on_timestamp_spam_callback + end + + def on_honeypot_spam_callback + return unless Feature.enabled?(:invisible_captcha) + + invisible_captcha_honeypot_counter.increment + log_request('Invisible_Captcha_Honeypot_Request') + + head(200) + end + + def on_timestamp_spam_callback + return unless Feature.enabled?(:invisible_captcha) + + invisible_captcha_timestamp_counter.increment + log_request('Invisible_Captcha_Timestamp_Request') + + redirect_to new_user_session_path, alert: InvisibleCaptcha.timestamp_error_message + end + + def invisible_captcha_honeypot_counter + @invisible_captcha_honeypot_counter ||= + Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_honeypot, + 'Counter of blocked sign up attempts with filled honeypot') + end + + def invisible_captcha_timestamp_counter + @invisible_captcha_timestamp_counter ||= + Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_timestamp, + 'Counter of blocked sign up attempts with invalid timestamp') + end + + def log_request(message) + request_information = { + message: message, + env: :invisible_captcha_signup_bot_detected, + ip: request.ip, + request_method: request.request_method, + fullpath: request.fullpath + } + + Gitlab::AuthLogger.error(request_information) + end +end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 2d46a71bf99..3b0abecf2c9 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -14,8 +14,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| - format.html - format.json { render json: cycle_analytics_json } + format.html do + Gitlab::UsageDataCounters::CycleAnalyticsCounter.count(:views) + + render :show + end + format.json do + render json: cycle_analytics_json + end end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index df9e55fda2a..5a1f93dc609 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:prometheus_computed_alerts) - push_frontend_feature_flag(:export_metrics_to_csv_enabled) end def index diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 3254229d9cb..c94fdd9483d 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -26,7 +26,7 @@ class Projects::RawController < Projects::ApplicationController limiter.log_request(request, :raw_blob_request_limit, current_user) flash[:alert] = _('You cannot access the raw file. Please wait a minute.') - redirect_to project_blob_path(@project, File.join(@ref, @path)) + redirect_to project_blob_path(@project, File.join(@ref, @path)), status: :too_many_requests end def raw_blob_request_limit diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 638934694e0..db10515c0b4 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify include AcceptsPendingInvitations include RecaptchaExperimentHelper + include InvisibleCaptcha prepend_before_action :check_captcha, only: :create before_action :whitelist_query_limiting, only: [:destroy] diff --git a/app/finders/remote_mirror_finder.rb b/app/finders/remote_mirror_finder.rb deleted file mode 100644 index 420db0077aa..00000000000 --- a/app/finders/remote_mirror_finder.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class RemoteMirrorFinder - attr_accessor :params - - def initialize(params) - @params = params - end - - # rubocop: disable CodeReuse/ActiveRecord - def execute - RemoteMirror.find_by(id: params[:id]) - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index acbcf0ded17..0ab19f1d2d2 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -270,7 +270,11 @@ module ApplicationSettingsHelper :diff_max_patch_bytes, :commit_email_hostname, :protected_ci_variables, - :local_markdown_version + :local_markdown_version, + :snowplow_collector_hostname, + :snowplow_cookie_domain, + :snowplow_enabled, + :snowplow_site_id ] end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 00000000000..af98a611b8b --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SessionsHelper + def unconfirmed_email? + flash[:alert] == t(:unconfirmed, scope: [:devise, :failure]) + end +end diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb index 51ea79d1ddd..221d9692661 100644 --- a/app/helpers/tracking_helper.rb +++ b/app/helpers/tracking_helper.rb @@ -2,6 +2,21 @@ module TrackingHelper def tracking_attrs(label, event, property) - {} # CE has no tracking features + return {} unless tracking_enabled? + + { + data: { + track_label: label, + track_event: event, + track_property: property + } + } + end + + private + + def tracking_enabled? + Rails.env.production? && + ::Gitlab::CurrentSettings.snowplow_enabled? end end diff --git a/app/mailers/emails/remote_mirrors.rb b/app/mailers/emails/remote_mirrors.rb index 2d8137843ec..f3938a052b0 100644 --- a/app/mailers/emails/remote_mirrors.rb +++ b/app/mailers/emails/remote_mirrors.rb @@ -3,7 +3,7 @@ module Emails module RemoteMirrors def remote_mirror_update_failed_email(remote_mirror_id, recipient_id) - @remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + @remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) @project = @remote_mirror.project mail(to: recipient(recipient_id, @project.group), subject: subject('Remote mirror update failed')) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cb6346421ec..2a99c6e5c59 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -99,6 +99,11 @@ class ApplicationSetting < ApplicationRecord presence: true, if: :plantuml_enabled + validates :snowplow_collector_hostname, + presence: true, + hostname: true, + if: :snowplow_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index b7a4d7aa803..55ac1e129cf 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -97,6 +97,10 @@ module ApplicationSettingImplementation usage_stats_set_by_user_id: nil, diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, commit_email_hostname: default_commit_email_hostname, + snowplow_collector_hostname: nil, + snowplow_cookie_domain: nil, + snowplow_enabled: false, + snowplow_site_id: nil, protected_ci_variables: false, local_markdown_version: 0, outbound_local_requests_whitelist: [], diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ac88d9714ac..f705e67121f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -384,7 +384,7 @@ module Ci return unless has_environment? strong_memoize(:expanded_environment_name) do - ExpandVariables.expand(environment, simple_variables) + ExpandVariables.expand(environment, -> { simple_variables }) end end diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 2fc1b67dfd2..6bd7473c8ff 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -64,11 +64,15 @@ module Clusters end def delete_private_key - "kubectl delete secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found" if private_key_name.present? + return unless private_key_name.present? + + args = %W(secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found) + + Gitlab::Kubernetes::KubectlCmd.delete(*args) end def delete_crd(definition) - "kubectl delete crd #{definition} --ignore-not-found" + Gitlab::Kubernetes::KubectlCmd.delete("crd", definition, "--ignore-not-found") end def cluster_issuer_file diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 3a175fec148..455cf200fbc 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -41,7 +41,7 @@ module Clusters extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id)) - applications = applications.present? ? applications.or(extra_apps) : extra_apps + applications = applications ? applications.or(extra_apps) : extra_apps end !applications.exists? diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 5eae23659ae..244fe738396 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -89,7 +89,7 @@ module Clusters def delete_knative_services cluster.kubernetes_namespaces.map do |kubernetes_namespace| - "kubectl delete ksvc --all -n #{kubernetes_namespace.namespace}" + Gitlab::Kubernetes::KubectlCmd.delete("ksvc", "--all", "-n", kubernetes_namespace.namespace) end end @@ -99,14 +99,14 @@ module Clusters def delete_knative_namespaces [ - "kubectl delete --ignore-not-found ns knative-serving", - "kubectl delete --ignore-not-found ns knative-build" + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-serving"), + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-build") ] end def delete_knative_and_istio_crds api_resources.map do |crd| - "kubectl delete --ignore-not-found crd #{crd}" + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "crd", "#{crd}") end end @@ -119,13 +119,13 @@ module Clusters def install_knative_metrics return [] unless cluster.application_prometheus_available? - ["kubectl apply -f #{METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.apply_file(METRICS_CONFIG)] end def delete_knative_istio_metrics return [] unless cluster.application_prometheus_available? - ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)] end def verify_cluster? diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 08e52f32bb3..f31a6b8b50e 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -106,13 +106,13 @@ module Clusters def install_knative_metrics return [] unless cluster.application_knative_available? - ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.apply_file(Clusters::Applications::Knative::METRICS_CONFIG)] end def delete_knative_istio_metrics return [] unless cluster.application_knative_available? - ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.delete("-f", Clusters::Applications::Knative::METRICS_CONFIG)] end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index a88cac6b8e6..4be4d95b4a1 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -40,8 +40,11 @@ class CommitStatus < ApplicationRecord scope :ordered, -> { order(:name) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } + scope :before_stage, -> (index) { where('stage_idx < ?', index) } + scope :for_stage, -> (index) { where(stage_idx: index) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) } + scope :for_ids, -> (ids) { where(id: ids) } scope :with_needs, -> (names = nil) do needs = Ci::BuildNeed.scoped_build.select(1) @@ -49,8 +52,10 @@ class CommitStatus < ApplicationRecord where('EXISTS (?)', needs).preload(:needs) end - scope :without_needs, -> do - where('NOT EXISTS (?)', Ci::BuildNeed.scoped_build.select(1)) + scope :without_needs, -> (names = nil) do + needs = Ci::BuildNeed.scoped_build.select(1) + needs = needs.where(name: names) if names + where('NOT EXISTS (?)', needs) end # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily @@ -149,6 +154,18 @@ class CommitStatus < ApplicationRecord end end + def self.names + select(:name) + end + + def self.status_for_prior_stages(index) + before_stage(index).latest.status || 'success' + end + + def self.status_for_names(names) + where(name: names).latest.status || 'success' + end + def locking_enabled? will_save_change_to_status? end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 27a5c3d5286..71ebb586c13 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -106,10 +106,15 @@ module HasStatus scope :running_or_pending, -> { with_status(:running, :pending) } scope :finished, -> { with_status(:success, :failed, :canceled) } scope :failed_or_canceled, -> { with_status(:failed, :canceled) } + scope :incomplete, -> { without_statuses(completed_statuses) } scope :cancelable, -> do where(status: [:running, :preparing, :pending, :created, :scheduled]) end + + scope :without_statuses, -> (names) do + with_status(all_state_names - names.to_a) + end end def started? diff --git a/app/models/project.rb b/app/models/project.rb index a6e43efa1f3..0c57ed3e43a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1230,6 +1230,14 @@ class Project < ApplicationRecord end end + def has_active_hooks?(hooks_scope = :push_hooks) + hooks.hooks_for(hooks_scope).any? || SystemHook.hooks_for(hooks_scope).any? + end + + def has_active_services?(hooks_scope = :push_hooks) + services.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend + end + def valid_repo? repository.exists? rescue diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 4edf263433f..a3793d9937b 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -68,7 +68,7 @@ module ChatMessage title_link: pipeline_url, fields: attachments_fields, footer: project.name, - footer_icon: project.avatar_url, + footer_icon: project.avatar_url(only_path: false), ts: finished_at }] end diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 5f5cff97808..cb16ad75d14 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -35,6 +35,8 @@ class SlashCommandsService < Service chat_user = find_chat_user(params) if chat_user&.user + return Gitlab::SlashCommands::Presenters::Access.new.access_denied unless chat_user.user.can?(:use_slash_commands) + Gitlab::SlashCommands::Command.new(project, chat_user, params).execute else url = authorize_chat_name_url(params) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 6b5605f9999..c9ee0653d86 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -4,6 +4,8 @@ class RemoteMirror < ApplicationRecord include AfterCommitQueue include MirrorAuthentication + MAX_FIRST_RUNTIME = 3.hours + MAX_INCREMENTAL_RUNTIME = 1.hour PROTECTED_BACKOFF_DELAY = 1.minute UNPROTECTED_BACKOFF_DELAY = 5.minutes @@ -31,11 +33,18 @@ class RemoteMirror < ApplicationRecord scope :enabled, -> { where(enabled: true) } scope :started, -> { with_update_status(:started) } - scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.hour.ago, 3.hours.ago) } + + scope :stuck, -> do + started + .where('(last_update_started_at < ? AND last_update_at IS NOT NULL)', + MAX_INCREMENTAL_RUNTIME.ago) + .or(where('(last_update_started_at < ? AND last_update_at IS NULL)', + MAX_FIRST_RUNTIME.ago)) + end state_machine :update_status, initial: :none do event :update_start do - transition [:none, :finished, :failed] => :started + transition any => :started end event :update_finish do @@ -46,9 +55,14 @@ class RemoteMirror < ApplicationRecord transition started: :failed end + event :update_retry do + transition started: :to_retry + end + state :started state :finished state :failed + state :to_retry after_transition any => :started do |remote_mirror, _| Gitlab::Metrics.add_event(:remote_mirrors_running) @@ -138,16 +152,27 @@ class RemoteMirror < ApplicationRecord end def updated_since?(timestamp) - last_update_started_at && last_update_started_at > timestamp && !update_failed? + return false if failed? + + last_update_started_at && last_update_started_at > timestamp end def mark_for_delete_if_blank_url mark_for_destruction if url.blank? end - def mark_as_failed(error_message) - update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message)) - update_fail + def update_error_message(error_message) + self.last_error = Gitlab::UrlSanitizer.sanitize(error_message) + end + + def mark_for_retry!(error_message) + update_error_message(error_message) + update_retry! + end + + def mark_as_failed!(error_message) + update_error_message(error_message) + update_fail! end def url=(value) @@ -190,6 +215,18 @@ class RemoteMirror < ApplicationRecord update_column(:error_notification_sent, true) end + def backoff_delay + if self.only_protected_branches + PROTECTED_BACKOFF_DELAY + else + UNPROTECTED_BACKOFF_DELAY + end + end + + def max_runtime + last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME + end + private def store_credentials @@ -219,14 +256,6 @@ class RemoteMirror < ApplicationRecord self.last_update_started_at >= Time.now - backoff_delay end - def backoff_delay - if self.only_protected_branches - PROTECTED_BACKOFF_DELAY - else - UNPROTECTED_BACKOFF_DELAY - end - end - def reset_fields update_columns( last_error: nil, diff --git a/app/models/repository.rb b/app/models/repository.rb index a89f573e3d6..9d45a12fa6e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -418,25 +418,29 @@ class Repository end # Runs code before pushing (= creating or removing) a tag. + # + # Note that this doesn't expire the tags. You may need to call + # expire_caches_for_tags or expire_tags_cache. def before_push_tag + repository_event(:push_tag) + end + + def expire_caches_for_tags expire_statistics_caches expire_emptiness_caches expire_tags_cache - - repository_event(:push_tag) end # Runs code before removing a tag. def before_remove_tag - expire_tags_cache - expire_statistics_caches + expire_caches_for_tags repository_event(:remove_tag) end # Runs code after removing a tag. def after_remove_tag - expire_tags_cache + expire_caches_for_tags end # Runs code after the HEAD of a repository is changed. @@ -460,8 +464,8 @@ class Repository end # Runs code after a new branch has been created. - def after_create_branch - expire_branches_cache + def after_create_branch(expire_cache: true) + expire_branches_cache if expire_cache repository_event(:push_branch) end @@ -474,8 +478,8 @@ class Repository end # Runs code after an existing branch has been removed. - def after_remove_branch - expire_branches_cache + def after_remove_branch(expire_cache: true) + expire_branches_cache if expire_cache end def method_missing(msg, *args, &block) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 134de1c9ace..311aab0dcd4 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -33,6 +33,7 @@ class GlobalPolicy < BasePolicy enable :access_git enable :receive_notifications enable :use_quick_actions + enable :use_slash_commands end rule { blocked | internal }.policy do @@ -40,6 +41,7 @@ class GlobalPolicy < BasePolicy prevent :access_api prevent :access_git prevent :receive_notifications + prevent :use_slash_commands end rule { required_terms_not_accepted }.policy do @@ -57,6 +59,7 @@ class GlobalPolicy < BasePolicy rule { access_locked }.policy do prevent :log_in + prevent :use_slash_commands end rule { ~(anonymous & restricted_public_level) }.policy do diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 99d4ff9ecd1..f4bd457ebc6 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -42,14 +42,19 @@ module Ci return false unless trigger_build_ids.present? return false unless Feature.enabled?(:ci_dag_support, project) - # rubocop: disable CodeReuse/ActiveRecord - trigger_build_names = pipeline.statuses - .where(id: trigger_build_ids) - .select(:name) - # rubocop: enable CodeReuse/ActiveRecord + # we find processables that are dependent: + # 1. because of current dependency, + trigger_build_names = pipeline.processables.latest + .for_ids(trigger_build_ids).names + # 2. does not have builds that not yet complete + incomplete_build_names = pipeline.processables.latest + .incomplete.names + + # Each found processable is guaranteed here to have completed status created_processables .with_needs(trigger_build_names) + .without_needs(incomplete_build_names) .find_each .map(&method(:process_build_with_needs)) .any? @@ -70,17 +75,13 @@ module Ci end end - # rubocop: disable CodeReuse/ActiveRecord def status_for_prior_stages(index) - pipeline.builds.where('stage_idx < ?', index).latest.status || 'success' + pipeline.processables.status_for_prior_stages(index) end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def status_for_build_needs(needs) - pipeline.builds.where(name: needs).latest.status || 'success' + pipeline.processables.status_for_names(needs) end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def stage_indexes_of_created_processables_without_needs @@ -89,12 +90,10 @@ module Ci end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def created_processables_in_stage_without_needs(index) created_processables_without_needs - .where(stage_idx: index) + .for_stage(index) end - # rubocop: enable CodeReuse/ActiveRecord def created_processables_without_needs if Feature.enabled?(:ci_dag_support, project) diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index d30df34e54b..1db18fcf401 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -19,7 +19,7 @@ module Git update_remote_mirrors - push_data + success end private @@ -33,7 +33,7 @@ module Git end def limited_commits - commits.last(PROCESS_COMMIT_LIMIT) + @limited_commits ||= commits.last(PROCESS_COMMIT_LIMIT) end def commits_count @@ -48,21 +48,25 @@ module Git [] end + # Push events in the activity feed only show information for the + # last commit. def create_events - EventCreateService.new.push(project, current_user, push_data) + EventCreateService.new.push(project, current_user, event_push_data) end def create_pipelines return unless params.fetch(:create_pipelines, true) Ci::CreatePipelineService - .new(project, current_user, push_data) + .new(project, current_user, base_params) .execute(:push, pipeline_options) end def execute_project_hooks - project.execute_hooks(push_data, hook_name) - project.execute_services(push_data, hook_name) + # Creating push_data invokes one CommitDelta RPC per commit. Only + # build this data if we actually need it. + project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name) + project.execute_services(push_data, hook_name) if project.has_active_services?(hook_name) end def enqueue_invalidate_cache @@ -73,18 +77,35 @@ module Git ) end - def push_data - @push_data ||= Gitlab::DataBuilder::Push.build( - project: project, - user: current_user, + def base_params + { oldrev: params[:oldrev], newrev: params[:newrev], ref: params[:ref], - commits: limited_commits, + push_options: params[:push_options] || {} + } + end + + def push_data_params(commits:, with_changed_files: true) + base_params.merge( + project: project, + user: current_user, + commits: commits, message: event_message, commits_count: commits_count, - push_options: params[:push_options] || {} + with_changed_files: with_changed_files ) + end + + def event_push_data + # We only need the last commit for the event push, and we don't + # need the full deltas either. + @event_push_data ||= Gitlab::DataBuilder::Push.build( + push_data_params(commits: commits.last, with_changed_files: false)) + end + + def push_data + @push_data ||= Gitlab::DataBuilder::Push.build(push_data_params(commits: limited_commits)) # Dependent code may modify the push data, so return a duplicate each time @push_data.dup diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index c41f445c3c4..431a5aedf2e 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -63,7 +63,7 @@ module Git end def branch_create_hooks - project.repository.after_create_branch + project.repository.after_create_branch(expire_cache: false) project.after_create_default_branch if default_branch? end @@ -78,7 +78,7 @@ module Git end def branch_remove_hooks - project.repository.after_remove_branch + project.repository.after_remove_branch(expire_cache: false) end # Schedules processing of commit messages diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index 1244a0f72a7..13a467a3ef9 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -2,31 +2,52 @@ module Projects class UpdateRemoteMirrorService < BaseService - attr_reader :errors + MAX_TRIES = 3 - def execute(remote_mirror) + def execute(remote_mirror, tries) return success unless remote_mirror.enabled? - errors = [] + update_mirror(remote_mirror) - begin - remote_mirror.ensure_remote! - repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) + success + rescue Gitlab::Git::CommandError => e + # This happens if one of the gitaly calls above fail, for example when + # branches have diverged, or the pre-receive hook fails. + retry_or_fail(remote_mirror, e.message, tries) - opts = {} - if remote_mirror.only_protected_branches? - opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) - end + error(e.message) + rescue => e + remote_mirror.mark_as_failed!(e.message) + raise e + end + + private + + def update_mirror(remote_mirror) + remote_mirror.update_start! + + remote_mirror.ensure_remote! + repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) - remote_mirror.update_repository(opts) - rescue => e - errors << e.message.strip + opts = {} + if remote_mirror.only_protected_branches? + opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) end - if errors.present? - error(errors.join("\n\n")) + remote_mirror.update_repository(opts) + + remote_mirror.update_finish! + end + + def retry_or_fail(mirror, message, tries) + if tries < MAX_TRIES + mirror.mark_for_retry!(message) else - success + # It's not likely we'll be able to recover from this ourselves, so we'll + # notify the users of the problem, and don't trigger any sidekiq retries + # Instead, we'll wait for the next change to try the push again, or until + # a user manually retries. + mirror.mark_as_failed!(message) end end end diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb index 49a7d0178f4..dcafebae52d 100644 --- a/app/services/update_deployment_service.rb +++ b/app/services/update_deployment_service.rb @@ -42,7 +42,7 @@ class UpdateDeploymentService return unless environment_url @expanded_environment_url = - ExpandVariables.expand(environment_url, variables) + ExpandVariables.expand(environment_url, -> { variables }) end def environment_url diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml new file mode 100644 index 00000000000..b60b5d55a1b --- /dev/null +++ b/app/views/admin/application_settings/_snowplow.html.haml @@ -0,0 +1,30 @@ +- expanded = true if !@application_setting.valid? && @application_setting.errors.any? { |k| k.to_s.start_with?('snowplow_') } +%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Snowplow') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') } + .settings-content + + = form_for @application_setting, url: integrations_admin_application_settings_path, html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :snowplow_enabled, class: 'form-check-input' + = f.label :snowplow_enabled, _('Enable snowplow tracking'), class: 'form-check-label' + .form-group + = f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light' + = f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com' + .form-group + = f.label :snowplow_site_id, _('Site ID'), class: 'label-light' + = f.text_field :snowplow_site_id, class: 'form-control' + .form-group + = f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light' + = f.text_field :snowplow_cookie_domain, class: 'form-control' + + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 2f10f08c839..0b1d3d1ddb3 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,20 +1,23 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: "user_login", class: 'label-bold' - = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' } + = f.label _('Username or email'), for: 'user_login', class: 'label-bold' + = f.text_field :login, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' } .form-group = f.label :password, class: 'label-bold' - = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' } + = f.password_field :password, class: 'form-control bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' } - if devise_mapping.rememberable? .remember-me - %label{ for: "user_remember_me" } + %label{ for: 'user_remember_me' } = f.check_box :remember_me, class: 'remember-me-checkbox' %span Remember me - .float-right.forgot-password - = link_to "Forgot your password?", new_password_path(:user) + .float-right + - if unconfirmed_email? + = link_to _('Resend confirmation email'), new_user_confirmation_path + - else + = link_to _('Forgot your password?'), new_password_path(:user) %div - if captcha_enabled? = recaptcha_tags .submit-container.move-submit-down - = f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' } + = f.submit _('Sign in'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' } diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 074edf645ba..2cd77af6877 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -5,6 +5,8 @@ = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = render "devise/shared/error_messages", resource: resource + - if Feature.enabled?(:invisible_captcha) + = invisible_captcha .name.form-group = f.label :name, _('Full name'), class: 'label-bold' = f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.") diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml new file mode 100644 index 00000000000..5f5c5e984c5 --- /dev/null +++ b/app/views/layouts/_snowplow.html.haml @@ -0,0 +1,29 @@ +- return unless Gitlab::CurrentSettings.snowplow_enabled? + += javascript_tag nonce: true do + :plain + ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; + p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) + };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; + n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow")); + + window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_hostname}', { + appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', + cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', + userFingerprint: false, + respectDoNotTrack: true, + forceSecureTracker: true, + post: true, + contexts: { webPage: true }, + stateStorageStrategy: "localStorage" + }); + + window.snowplow('enableActivityTracking', 30, 30); + window.snowplow('trackPageView'); + +- return unless Feature.enabled?(:additional_snowplow_tracking, @group) + += javascript_tag nonce: true do + :plain + window.snowplow('enableFormTracking'); + window.snowplow('enableLinkClickTracking'); diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index d16e2dddbe0..d99063e344f 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -45,20 +45,20 @@ .form-group = f.label :layout, class: 'label-bold' do = s_('Preferences|Layout width') - = f.select :layout, layout_choices, {}, class: 'form-control' + = f.select :layout, layout_choices, {}, class: 'select2' .form-text.text-muted = s_('Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout.') .form-group = f.label :dashboard, class: 'label-bold' do = s_('Preferences|Default dashboard') - = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + = f.select :dashboard, dashboard_choices, {}, class: 'select2' = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific .form-group = f.label :project_view, class: 'label-bold' do = s_('Preferences|Project overview content') - = f.select :project_view, project_view_choices, {}, class: 'form-control' + = f.select :project_view, project_view_choices, {}, class: 'select2' .form-text.text-muted = s_('Preferences|Choose what content you want to see on a project’s overview page.') @@ -82,7 +82,7 @@ .form-group = f.label :first_day_of_week, class: 'label-bold' do = _('First day of the week') - = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2' - if Feature.enabled?(:user_time_settings) .col-sm-12 %hr diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 280ec6d715b..eb100e5cf47 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -43,7 +43,8 @@ = _('Mirrored repositories') = render_if_exists 'projects/mirrors/mirrored_repositories_count' %th= _('Direction') - %th= _('Last update') + %th= _('Last update attempt') + %th= _('Last successful update') %th %th %tbody.js-mirrors-table-body @@ -53,6 +54,8 @@ %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } %td.qa-mirror-repository-url= mirror.safe_url %td= _('Push') + %td + = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td - if mirror.disabled? diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index efabb7f7b19..149b0d6cddd 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -2,7 +2,7 @@ .col-sm-12 = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group - = label_tag :user_ids, _("Select members to invite"), class: "label-bold" + = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold" = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, _("Choose a role permission"), class: "label-bold" diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 70f1bf8ef46..c5eecc900b2 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -4,7 +4,7 @@ location.reload(); - else :plain - $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); + $(".flash-container").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); $('.edit-project .js-btn-success-general-project-settings').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 66a614b0197..858731b2dda 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -14,7 +14,7 @@ .col-sm-12 = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title - if @page.persisted? - %span.edit-wiki-page-slug-tip + %span.d-inline-block.mw-100.prepend-top-5 = icon('lightbulb-o') = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index dc12e368b35..2c675c0de9c 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -11,7 +11,7 @@ = label_tag :new_wiki_path do %span= s_("WikiPage|Page slug") = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true - %span.new-wiki-page-slug-tip + %span.d-inline-block.mw-100.prepend-top-5 = icon('lightbulb-o') = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") .form-actions diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 1277ea6c743..e8b59a3b8c4 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -5,7 +5,7 @@ = wiki_page_errors(@error) -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 8c2cbd495a0..009133be117 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,7 +1,7 @@ - @content_class = "limit-container-width" unless fluid_layout - page_title s_("WikiClone|Git Access"), _("Wiki") -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index c5fbeeafa54..f8468ef9a78 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,6 +1,6 @@ - page_title _("History"), @page.human_title, _("Wiki") -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 2191e5ab287..f7999c3f1bd 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -5,7 +5,7 @@ - sort_title = wiki_sort_title(params[:sort]) %div{ class: container_class } - .wiki-page-header + .wiki-page-header.top-area .nav-text.flex-fill %h2.wiki-page-title diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 95cd3356ec8..1d649886331 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -4,7 +4,7 @@ - page_title @page.human_title, _("Wiki") - add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 400becdd023..991a177018e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -88,7 +88,6 @@ - pipeline_processing:ci_build_prepare - pipeline_processing:build_queue - pipeline_processing:build_success -- pipeline_processing:build_process - pipeline_processing:pipeline_process - pipeline_processing:pipeline_success - pipeline_processing:pipeline_update diff --git a/app/workers/build_process_worker.rb b/app/workers/build_process_worker.rb deleted file mode 100644 index 9cd9519df1f..00000000000 --- a/app/workers/build_process_worker.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class BuildProcessWorker - include ApplicationWorker - include PipelineQueue - - queue_namespace :pipeline_processing - - # rubocop: disable CodeReuse/ActiveRecord - def perform(build_id) - CommitStatus.find_by(id: build_id).try do |build| - build.pipeline.process!([build_id]) - end - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index dba7837bd12..622bd6f1f48 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -42,6 +42,11 @@ class PostReceive user = identify_user(post_received) return false unless user + # Expire the branches cache so we have updated data for this push + post_received.project.repository.expire_branches_cache if post_received.includes_branches? + # We only need to expire tags once per push + post_received.project.repository.expire_caches_for_tags if post_received.includes_tags? + post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index| service_klass = if Gitlab::Git.tag_ref?(ref) @@ -72,6 +77,7 @@ class PostReceive def after_project_changes_hooks(post_received, user, refs, changes) hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) + Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes) end def process_wiki_changes(post_received) diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 5bafe8e2046..368abfeda99 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -4,7 +4,7 @@ class RemoteMirrorNotificationWorker include ApplicationWorker def perform(remote_mirror_id) - remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) # We check again if there's an error because a newer run since this job was # fired could've completed successfully. diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb index 03a7ff2cd7a..d13c7641eb3 100644 --- a/app/workers/repository_update_remote_mirror_worker.rb +++ b/app/workers/repository_update_remote_mirror_worker.rb @@ -1,50 +1,53 @@ # frozen_string_literal: true class RepositoryUpdateRemoteMirrorWorker - UpdateAlreadyInProgressError = Class.new(StandardError) UpdateError = Class.new(StandardError) include ApplicationWorker + include Gitlab::ExclusiveLeaseHelpers sidekiq_options retry: 3, dead: false - sidekiq_retry_in { |count| 30 * count } + LOCK_WAIT_TIME = 30.seconds + MAX_TRIES = 3 - sidekiq_retries_exhausted do |msg, _| - Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}" - end - - def perform(remote_mirror_id, scheduled_time) - remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + def perform(remote_mirror_id, scheduled_time, tries = 0) + remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) + return unless remote_mirror return if remote_mirror.updated_since?(scheduled_time) - raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress? + # If the update is already running, wait for it to finish before running again + # This will wait for a total of 90 seconds in 3 steps + in_lock(remote_mirror_update_lock(remote_mirror.id), + retries: 3, + ttl: remote_mirror.max_runtime, + sleep_sec: LOCK_WAIT_TIME) do + update_mirror(remote_mirror, scheduled_time, tries) + end + rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + # If an update runs longer than 1.5 minutes, we'll reschedule it + # with a backoff. The next run will check if the previous update would + # include the changes that triggered this update and become a no-op. + self.class.perform_in(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, tries) + end - remote_mirror.update_start + private - project = remote_mirror.project + def update_mirror(mirror, scheduled_time, tries) + project = mirror.project current_user = project.creator - result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror) - raise UpdateError, result[:message] if result[:status] == :error - - remote_mirror.update_finish - rescue UpdateAlreadyInProgressError - raise - rescue UpdateError => ex - fail_remote_mirror(remote_mirror, ex.message) - raise - rescue => ex - return unless remote_mirror + result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(mirror, tries) - fail_remote_mirror(remote_mirror, ex.message) - raise UpdateError, "#{ex.class}: #{ex.message}" + if result[:status] == :error && mirror.to_retry? + schedule_retry(mirror, scheduled_time, tries) + end end - private - - def fail_remote_mirror(remote_mirror, message) - remote_mirror.mark_as_failed(message) + def remote_mirror_update_lock(mirror_id) + [self.class.name, mirror_id].join(':') + end - Rails.logger.error(message) # rubocop:disable Gitlab/RailsLogger + def schedule_retry(mirror, scheduled_time, tries) + self.class.perform_in(mirror.backoff_delay, mirror.id, scheduled_time, tries + 1) end end diff --git a/bin/changelog b/bin/changelog index 328d9495b96..ec068d06507 100755 --- a/bin/changelog +++ b/bin/changelog @@ -23,7 +23,7 @@ module ChangelogHelpers Abort = Class.new(StandardError) Done = Class.new(StandardError) - MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters + MAX_FILENAME_LENGTH = 99 # GNU tar has a 99 character limit def capture_stdout(cmd) output = IO.popen(cmd, &:read) diff --git a/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml new file mode 100644 index 00000000000..f249eff572c --- /dev/null +++ b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml @@ -0,0 +1,5 @@ +--- +title: Adjust copy for adding additional members +merge_request: 31726 +author: +type: changed diff --git a/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml new file mode 100644 index 00000000000..ccfd929b6ba --- /dev/null +++ b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Track page views for cycle analytics show page +merge_request: 31717 +author: +type: added diff --git a/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml new file mode 100644 index 00000000000..f0cc7fe9b6d --- /dev/null +++ b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml @@ -0,0 +1,6 @@ +--- +title: Updated the personal access token api scope description to reflect the permissions + it grants +merge_request: 31759 +author: +type: other diff --git a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml index 674d53286e6..674d53286e6 100644 --- a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml +++ b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml diff --git a/changelogs/unreleased/59829-fix-style-lint-wiki.yml b/changelogs/unreleased/59829-fix-style-lint-wiki.yml new file mode 100644 index 00000000000..48242a77c6b --- /dev/null +++ b/changelogs/unreleased/59829-fix-style-lint-wiki.yml @@ -0,0 +1,5 @@ +--- +title: Fix the style-lint errors and warnings for `app/assets/stylesheets/pages/wiki.scss` +merge_request: 31656 +author: +type: other diff --git a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml index ec6e9c5aff8..ec6e9c5aff8 100644 --- a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml +++ b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml diff --git a/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml new file mode 100644 index 00000000000..10f2b7eaed5 --- /dev/null +++ b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml @@ -0,0 +1,5 @@ +--- +title: Harmonize selections in user settings +merge_request: 31110 +author: Marc Schwede +type: other diff --git a/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml new file mode 100644 index 00000000000..21771c76873 --- /dev/null +++ b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml @@ -0,0 +1,5 @@ +--- +title: 'feat: adds a download to csv functionality to the dropdown in prometheus metrics' +merge_request: 31679 +author: +type: changed diff --git a/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml new file mode 100644 index 00000000000..a5f62dbcd56 --- /dev/null +++ b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml @@ -0,0 +1,5 @@ +--- +title: Allow users to resend a confirmation link when the grace period has expired +merge_request: 31476 +author: +type: changed diff --git a/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml new file mode 100644 index 00000000000..217db8aa05a --- /dev/null +++ b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml @@ -0,0 +1,5 @@ +--- +title: Invalidate branches cache on PostReceive +merge_request: 31653 +author: +type: fixed diff --git a/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml new file mode 100644 index 00000000000..df0ac649ac1 --- /dev/null +++ b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml @@ -0,0 +1,5 @@ +--- +title: Fix project avatar image in Slack pipeline notifications +merge_request: 31788 +author: +type: fixed diff --git a/changelogs/unreleased/bump_helm_kubectl_gitlab.yml b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml new file mode 100644 index 00000000000..d768462e130 --- /dev/null +++ b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml @@ -0,0 +1,5 @@ +--- +title: Bump Helm to 2.14.3 and kubectl to 1.11.10 for Kubernetes integration +merge_request: 31716 +author: +type: other diff --git a/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml new file mode 100644 index 00000000000..962376086b0 --- /dev/null +++ b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml @@ -0,0 +1,6 @@ +--- +title: Retry push mirrors faster when running concurrently, improve error handling + when push mirrors fail +merge_request: 31247 +author: +type: changed diff --git a/changelogs/unreleased/id-source-code-smau.yml b/changelogs/unreleased/id-source-code-smau.yml new file mode 100644 index 00000000000..6ba5068544e --- /dev/null +++ b/changelogs/unreleased/id-source-code-smau.yml @@ -0,0 +1,5 @@ +--- +title: Add usage pings for source code pushes +merge_request: 31734 +author: +type: added diff --git a/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml new file mode 100644 index 00000000000..606c60721b8 --- /dev/null +++ b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml @@ -0,0 +1,5 @@ +--- +title: error message for general settings +merge_request: 31636 +author: Mesut Güneş +type: fixed diff --git a/changelogs/unreleased/post-migrate-private-profile.yml b/changelogs/unreleased/post-migrate-private-profile.yml new file mode 100644 index 00000000000..53a55661aa0 --- /dev/null +++ b/changelogs/unreleased/post-migrate-private-profile.yml @@ -0,0 +1,5 @@ +--- +title: Migrate remaining users with null private_profile +merge_request: 31708 +author: +type: other diff --git a/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml new file mode 100644 index 00000000000..cd31fe0f35c --- /dev/null +++ b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml @@ -0,0 +1,5 @@ +--- +title: Restrict slash commands to users who can log in +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml new file mode 100644 index 00000000000..502fc22ebbd --- /dev/null +++ b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml @@ -0,0 +1,5 @@ +--- +title: Only expire tag cache once per push +merge_request: 31641 +author: +type: performance diff --git a/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml new file mode 100644 index 00000000000..cd63b9bf425 --- /dev/null +++ b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml @@ -0,0 +1,5 @@ +--- +title: Reduce Gitaly calls in PostReceive +merge_request: 31741 +author: +type: performance diff --git a/config/application.rb b/config/application.rb index 733f8652286..2554dd8cca2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,11 +41,6 @@ module Gitlab #{config.root}/app/models/hooks #{config.root}/app/models/members #{config.root}/app/models/project_services - #{config.root}/app/workers/concerns - #{config.root}/app/policies/concerns - #{config.root}/app/services/concerns - #{config.root}/app/serializers/concerns - #{config.root}/app/finders/concerns #{config.root}/app/graphql/resolvers/concerns #{config.root}/app/graphql/mutations/concerns]) diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb index 2ecb7956007..a1abb29838b 100644 --- a/config/initializers/elastic_client_setup.rb +++ b/config/initializers/elastic_client_setup.rb @@ -5,46 +5,42 @@ require 'gitlab/current_settings' Gitlab.ee do + require 'elasticsearch/model' + + ### Modified from elasticsearch-model/lib/elasticsearch/model.rb + + [ + Elasticsearch::Model::Client::ClassMethods, + Elasticsearch::Model::Naming::ClassMethods, + Elasticsearch::Model::Indexing::ClassMethods, + Elasticsearch::Model::Searching::ClassMethods + ].each do |mod| + Elasticsearch::Model::Proxy::ClassMethodsProxy.include mod + end + + [ + Elasticsearch::Model::Client::InstanceMethods, + Elasticsearch::Model::Naming::InstanceMethods, + Elasticsearch::Model::Indexing::InstanceMethods, + Elasticsearch::Model::Serializing::InstanceMethods + ].each do |mod| + Elasticsearch::Model::Proxy::InstanceMethodsProxy.include mod + end + + Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def as_indexed_json(options={}) + target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super + end + CODE + + ### Monkey patches + Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods - - module Elasticsearch - module Model - module Client - # This mutex is only used to synchronize *creation* of a new client, so - # all including classes can share the same client instance - CLIENT_MUTEX = Mutex.new - - cattr_accessor :cached_client - cattr_accessor :cached_config - - module ClassMethods - # Override the default ::Elasticsearch::Model::Client implementation to - # return a client configured from application settings. All including - # classes will use the same instance, which is refreshed automatically - # if the settings change. - # - # _client is present to match the arity of the overridden method, where - # it is also not used. - # - # @return [Elasticsearch::Transport::Client] - def client(_client = nil) - store = ::Elasticsearch::Model::Client - - store::CLIENT_MUTEX.synchronize do - config = Gitlab::CurrentSettings.elasticsearch_config - - if store.cached_client.nil? || config != store.cached_config - store.cached_client = ::Gitlab::Elastic::Client.build(config) - store.cached_config = config - end - end - - store.cached_client - end - end - end - end - end + Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing + Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client end diff --git a/config/initializers/invisible_captcha.rb b/config/initializers/invisible_captcha.rb new file mode 100644 index 00000000000..5177c730596 --- /dev/null +++ b/config/initializers/invisible_captcha.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +InvisibleCaptcha.setup do |config| + config.honeypots = %w(firstname lastname) + config.timestamp_enabled = true + config.timestamp_threshold = 4 +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index a8234263275..258d8a99986 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -69,7 +69,7 @@ en: email: Allows read-only access to the user's primary email address using OpenID Connect scope_desc: api: - Grants complete read/write access to the API, including all groups and projects. + Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. read_user: Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. read_repository: diff --git a/config/locales/invisible_captcha.en.yml b/config/locales/invisible_captcha.en.yml new file mode 100644 index 00000000000..5978549c0c3 --- /dev/null +++ b/config/locales/invisible_captcha.en.yml @@ -0,0 +1,4 @@ +en: + invisible_captcha: + sentence_for_humans: If you are human, please ignore this field. + timestamp_error_message: That was a bit too quick! Please resubmit. diff --git a/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb new file mode 100644 index 00000000000..2d3243f3357 --- /dev/null +++ b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddObjectStorageFlagToGeoNode < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false + end + + def down + remove_column :geo_nodes, :sync_object_storage + end +end diff --git a/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb new file mode 100644 index 00000000000..063c1e16c27 --- /dev/null +++ b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class MigratePrivateProfileNulls < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DELAY = 5.minutes.to_i + BATCH_SIZE = 1_000 + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + + include ::EachBatch + end + + def up + # Migration will take about 7 hours + User.where(private_profile: nil).each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql("MIN(id)"), Arel.sql("MAX(id)")).first + delay = index * DELAY + + BackgroundMigrationWorker.perform_in(delay.seconds, 'MigrateNullPrivateProfileToFalse', [*range]) + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 591758af0e4..7c4a91da750 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_06_071559) do +ActiveRecord::Schema.define(version: 2019_08_12_070645) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -1456,6 +1456,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.integer "container_repositories_max_capacity", default: 10, null: false t.datetime_with_timezone "created_at" t.datetime_with_timezone "updated_at" + t.boolean "sync_object_storage", default: false, null: false t.index ["access_key"], name: "index_geo_nodes_on_access_key" t.index ["name"], name: "index_geo_nodes_on_name", unique: true t.index ["primary"], name: "index_geo_nodes_on_primary" diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index e418938451a..d0adeb89543 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -669,6 +669,39 @@ To get around this, you can [change the group path](../user/group/index.md#chang branch name. Another option is to create a [push rule](../push_rules/push_rules.html) to prevent this at the instance level. +### Image push errors + +When getting errors or "retrying" loops in an attempt to push an image but `docker login` works fine, +there is likely an issue with the headers forwarded to the registry by NGINX. The default recommended +NGINX configurations should handle this, but it might occur in custom setups where the SSL is +offloaded to a third party reverse proxy. + +This problem was discussed in a [docker project issue][docker-image-push-issue] and a simple solution +would be to enable relative urls in the registry. + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + registry['env'] = { + "REGISTRY_HTTP_RELATIVEURLS" => true + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**For installations from source** + +1. Edit the YML configuration file you created when you [deployed the registry][registry-deploy]. Add the following snippet: + + ```yaml + http: + relativeurls: true + ``` + +1. Restart the registry for the changes to take affect. + [ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239 [docker-insecure-self-signed]: https://docs.docker.com/registry/insecure/#use-self-signed-certificates [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure @@ -687,3 +720,4 @@ this at the instance level. [new-domain]: #configure-container-registry-under-its-own-domain [notifications-config]: https://docs.docker.com/registry/notifications/ [registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications +[docker-image-push-issue]: https://github.com/docker/distribution/issues/970 diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md new file mode 100644 index 00000000000..8e33cea6217 --- /dev/null +++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md @@ -0,0 +1,36 @@ +# GitLab instance administration project + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/56883) in GitLab 12.2. + +GitLab has been adding the ability for administrators to see insights into the health of +their GitLab instance. In order to surface this experience in a native way, similar to how +you would interact with an application deployed via GitLab, a base project called +"GitLab Instance Administration" with +[internal visibility](../../../public_access/public_access.md#internal-projects) will be +added under a group called "GitLab Instance Administrators" specifically created for +visualizing and configuring the monitoring of your GitLab instance. + +All administrators at the time of creation of the project and group will be added +as maintainers of the group and project, and as an admin, you'll be able to add new +members to the group in order to give them maintainer access to the project. + +This project will be used for self-monitoring your GitLab instance. + +## Connection to Prometheus + +The project will be automatically configured to connect to the +[internal Prometheus](../prometheus/index.md) instance if the Prometheus +instance is present (should be the case if GitLab was installed via Omnibus +and you haven't disabled it). + +If that's not the case or if you have an external Prometheus instance or an HA setup, +you should +[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus). + +## Taking action on Prometheus alerts **[ULTIMATE]** + +You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances) +to the Prometheus config in order for GitLab to receive notifications of any alerts. + +Once the webhook is setup, you can +[take action on incoming alerts](../../../user/project/integrations/prometheus.md#taking-action-on-incidents-ultimate). diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md index fa0459b24ff..2b3daec42bd 100644 --- a/doc/administration/monitoring/index.md +++ b/doc/administration/monitoring/index.md @@ -2,6 +2,9 @@ Explore our features to monitor your GitLab instance: +- [GitLab self-monitoring](gitlab_instance_administration_project/index.md): The + GitLab instance administration project helps to monitor the GitLab instance and + take action on alerts. - [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance. - [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products. - [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics. diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index e3af5d60533..d0b33ab467f 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -10,7 +10,7 @@ GET /geo_nodes ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes ``` Example response: @@ -27,8 +27,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } }, { "id": 2, @@ -40,8 +47,17 @@ Example response: "current": false, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "sync_object_storage": true, + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit", + "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects", + "_links": { + "self":"https://primary.example.com/api/v4/geo_nodes/2", + "status":"https://primary.example.com/api/v4/geo_nodes/2/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair" + } } ] ``` @@ -53,7 +69,7 @@ GET /geo_nodes/:id ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1 +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/1 ``` Example response: @@ -69,8 +85,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } } ``` @@ -84,16 +107,18 @@ _This can only be run against a primary Geo node._ PUT /geo_nodes/:id ``` -| Attribute | Type | Required | Description | -|----------------------|---------|-----------|---------------------------------------------------------------------------| -| `id` | integer | yes | The ID of the Geo node. | -| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | -| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | -| `url` | string | yes | The user-facing URL of the Geo node. | -| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| -| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | -| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | -| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | +| Attribute | Type | Required | Description | +|-----------------------------|---------|-----------|---------------------------------------------------------------------------| +| `id` | integer | yes | The ID of the Geo node. | +| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | +| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | +| `url` | string | yes | The user-facing URL of the Geo node. | +| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| +| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | +| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | +| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | +| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. | +| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. | Example response: @@ -108,8 +133,17 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "sync_object_storage": true, + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit", + "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects", + "_links": { + "self":"https://primary.example.com/api/v4/geo_nodes/2", + "status":"https://primary.example.com/api/v4/geo_nodes/2/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair" + } } ``` @@ -151,8 +185,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } } ``` @@ -163,7 +204,7 @@ GET /geo_nodes/status ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/status ``` Example response: @@ -314,7 +355,7 @@ GET /geo_nodes/:id/status ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/2/status ``` Example response: @@ -388,7 +429,7 @@ GET /geo_nodes/current/failures This endpoint uses [Pagination](README.md#pagination). ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/current/failures ``` Example response: diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md index 9c91264ed65..b1b75abe32f 100644 --- a/doc/api/releases/links.md +++ b/doc/api/releases/links.md @@ -21,7 +21,7 @@ GET /projects/:id/releases/:tag_name/assets/links Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" ``` Example response: @@ -60,7 +60,7 @@ GET /projects/:id/releases/:tag_name/assets/links/:link_id Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: @@ -96,7 +96,7 @@ curl --request POST \ --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ --data name="awesome-v0.2.dmg" \ --data url="http://192.168.10.15:3000" \ - "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" + "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" ``` Example response: @@ -132,7 +132,7 @@ You have to specify at least one of `name` or `url` Example request: ```sh -curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: @@ -163,7 +163,7 @@ DELETE /projects/:id/releases/:tag_name/assets/links/:link_id Example request: ```sh -curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: diff --git a/doc/api/settings.md b/doc/api/settings.md index 83125aff264..248d19461f6 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -321,4 +321,8 @@ are listed in the descriptions of the relevant settings. | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | | `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | +| `snowplow_enabled` | boolean | no | Enable snowplow tracking. | +| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) | +| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) | +| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) | | `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | diff --git a/doc/ci/README.md b/doc/ci/README.md index f3006528d01..ca9d0aa61bd 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -16,7 +16,7 @@ through the [continuous methodologies](introduction/index.md#introduction-to-cic NOTE: **Out-of-the-box management systems can decrease hours spent on maintaining toolchains by 10% or more.** Watch our ["Mastering continuous software development"](https://about.gitlab.com/webcast/mastering-ci-cd/) -webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development. +webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development. ## Overview @@ -67,11 +67,11 @@ to your needs: For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide. Once you're familiar with how GitLab CI/CD works, see the -[`. gitlab-ci.yml` full reference](yaml/README.md) +[`.gitlab-ci.yml` full reference](yaml/README.md) for all the attributes you can set and use. NOTE: **Note:** -GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota-free-only). +GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/gitlab_com/index.md#shared-runners). ## Configuration diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 9e1a62bae71..f6c47a99712 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -619,6 +619,10 @@ versions of the app, all without leaving GitLab. Add a [button to the Monitoring dashboard](../user/project/operations/linking_to_an_external_dashboard.md) linking directly to your existing external dashboards. +#### Embedding metrics in GitLab Flavored Markdown + +Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details. + ### Web terminals > Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners. diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index c3678fc948e..8474d4ef66e 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -319,21 +319,21 @@ How this feature will work: 1. You set the _maximum job timeout_ for a Runner to 24 hours 1. You set the _CI/CD Timeout_ for a project to **2 hours** 1. You start a job -1. The job, if running longer, will be timeouted after **2 hours** +1. The job, if running longer, will be timed out after **2 hours** **Example 2 - Runner timeout not configured** 1. You remove the _maximum job timeout_ configuration from a Runner 1. You set the _CI/CD Timeout_ for a project to **2 hours** 1. You start a job -1. The job, if running longer, will be timeouted after **2 hours** +1. The job, if running longer, will be timed out after **2 hours** **Example 3 - Runner timeout smaller than project timeout** 1. You set the _maximum job timeout_ for a Runner to **30 minutes** 1. You set the _CI/CD Timeout_ for a project to 2 hours 1. You start a job -1. The job, if running longer, will be timeouted after **30 minutes** +1. The job, if running longer, will be timed out after **30 minutes** ### Be careful with sensitive information diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md index 0965db29557..635895051bc 100644 --- a/doc/development/elasticsearch.md +++ b/doc/development/elasticsearch.md @@ -148,6 +148,36 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/ - Searches can have their own analyzers. Remember to check when editing analyzers - `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches +## Architecture + +GitLab uses `elasticsearch-rails` for handling communication with Elasticsearch server. However, in order to achieve zero-downtime deployment during schema changes, an extra abstraction layer is built to allow: + +* Indexing (writes) to multiple indexes, with different mappings +* Switching to different index for searches (reads) on the fly + +Currently we are on the process of migrating models to this new design (e.g. `Snippet`), and it is hardwired to work with a single version for now. + +Traditionally, `elasticsearch-rails` provides class and instance level `__elasticsearch__` proxy methods. If you call `Issue.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`, and if you call `Issue.first.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`. These proxy objects would talk to Elasticsearch server directly. + +In the new design, `__elasticsearch__` instead represents one extra layer of proxy. It would keep multiple versions of the actual proxy objects, and it would forward read and write calls to the proxy of the intended version. + +The `elasticsearch-rails`'s way of specifying each model's mappings and other settings is to create a module for the model to include. However in the new design, each model would have its own corresponding subclassed proxy object, where the settings reside in. For example, snippet related setting in the past reside in `SnippetsSearch` module, but in the new design would reside in `SnippetClassProxy` (which is a subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy`). This reduces namespace pollution in model classes. + +The global configurations per version are now in the `Elastic::(Version)::Config` class. You can change mappings there. + +### Creating new version of schema + +Currently GitLab would still work with a single version of setting. Once it is implemented, multiple versions of setting can exists in different folders (e.g. `ee/lib/elastic/v12p1` and `ee/lib/elastic/v12p3`). To keep a continuous git history, the latest version lives under the `/latest` folder, but is aliased as the latest version. + +If the current version is `v12p1`, and we need to create a new version for `v12p3`, the steps are as follows: + +1. Copy the entire folder of `v12p1` as `v12p3` +1. Change the namespace for files under `v12p3` folder from `V12p1` to `V12p3` (which are still aliased to `Latest`) +1. Delete `v12p1` folder +1. Copy the entire folder of `latest` as `v12p1` +1. Change the namespace for files under `v12p1` folder from `Latest` to `V12p1` +1. Make changes to `Latest` as needed + ## Troubleshooting ### Getting `flood stage disk watermark [95%] exceeded` diff --git a/doc/development/fe_guide/event_tracking.md b/doc/development/fe_guide/event_tracking.md index 715d91c6db6..1b417d4c8c2 100644 --- a/doc/development/fe_guide/event_tracking.md +++ b/doc/development/fe_guide/event_tracking.md @@ -1,6 +1,8 @@ -# Event Tracking +# Event tracking -We use a tracking interface that wraps up [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. Snowplow implements page tracking, but also exposes custom event tracking. +GitLab provides `Tracking`, an interface that wraps +[Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. +It uses Snowplow's custom event tracking functions. The tracking interface can be imported in JS files as follows: diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 6bf6113dd81..deaef8e768b 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -17,6 +17,8 @@ Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn For our currently-supported browsers, see our [requirements][requirements]. +Use [BrowserStack](https://www.browserstack.com/) to test with our supported browsers. Login to BrowserStack with the credentials saved in GitLab's [shared 1Password account](https://about.gitlab.com/handbook/security/#1password-for-teams). + ## Initiatives Current high-level frontend goals are listed on [Frontend Epics](https://gitlab.com/groups/gitlab-org/-/epics?label_name%5B%5D=frontend). diff --git a/doc/install/installation.md b/doc/install/installation.md index 72a3514e2d5..295d9804497 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -172,7 +172,7 @@ sudo make install # Download and compile from source cd /tmp curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.22.0.tar.gz -echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b' git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz +echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz cd git-2.22.0/ ./configure --with-libpcre make prefix=/usr/local all @@ -202,8 +202,8 @@ Then select 'Internet Site' and press enter to confirm the hostname. The Ruby interpreter is required to run GitLab. -**Note:** The current supported Ruby (MRI) version is 2.5.x. GitLab 11.6 - dropped support for Ruby 2.4.x. +**Note:** The current supported Ruby (MRI) version is 2.6.x. GitLab 12.2 + dropped support for Ruby 2.5.x. The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab in production, frequently leads to hard to diagnose problems. For example, diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 83a9e7fe294..234e5acb394 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -40,7 +40,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.5. Support for Ruby versions below 2.5 (2.3, 2.4) will stop with GitLab 11.6. +GitLab requires Ruby (MRI) 2.6. Support for Ruby versions below 2.6 (2.4, 2.5) will stop with GitLab 12.2. You will have to use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab diff --git a/doc/user/admin_area/settings/img/additional_minutes.png b/doc/subscriptions/img/additional_minutes.png Binary files differindex b159b98c9ce..b159b98c9ce 100644 --- a/doc/user/admin_area/settings/img/additional_minutes.png +++ b/doc/subscriptions/img/additional_minutes.png diff --git a/doc/user/admin_area/settings/img/buy_btn.png b/doc/subscriptions/img/buy_btn.png Binary files differindex 4fd05c0fba7..4fd05c0fba7 100644 --- a/doc/user/admin_area/settings/img/buy_btn.png +++ b/doc/subscriptions/img/buy_btn.png diff --git a/doc/user/admin_area/settings/img/buy_minutes_card.png b/doc/subscriptions/img/buy_minutes_card.png Binary files differindex cab098300cd..cab098300cd 100644 --- a/doc/user/admin_area/settings/img/buy_minutes_card.png +++ b/doc/subscriptions/img/buy_minutes_card.png diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 9317ba0c71e..13c406727ab 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -221,6 +221,52 @@ The following table describes details of your subscription for groups: | Subscription start date | Date your subscription started. If this is for a Free plan, is the date you transitioned off your group's paid plan. | | Subscription end date | Date your current subscription will end. Does not apply to Free plans. | +#### Extra Shared Runners pipeline minutes + +If you're using GitLab.com, you can purchase additional CI minutes so your +pipelines will not be blocked after you have used all your CI minutes from your +main quota. Additional minutes: + +- Are only used once the shared quota included in your subscription runs out. +- Roll over month to month. + +In order to purchase additional minutes, you should follow these steps: + +1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**. + + ![Buy additional minutes](img/buy_btn.png) + +1. Locate the subscription card that is linked to your group on GitLab.com, + click on **Buy more CI minutes**, and complete the details about the transaction. + + ![Buy additional minutes](img/buy_minutes_card.png) + +1. Once we have processed your payment, the extra CI minutes + will be synced to your Group and you can visualize it from the + **Group > Settings > Pipelines quota** page: + + ![Additional minutes](img/additional_minutes.png) + +Be aware that: + +1. If you have purchased extra CI minutes before the purchase of a paid plan, + we will calculate a pro-rated charge for your paid plan. That means you may + be charged for less than one year since your subscription was previously + created with the extra CI minutes. +1. Once the extra CI minutes has been assigned to a Group they cannot be transferred + to a different Group. +1. If you have some minutes used over your default quota, these minutes will + be deducted from your Additional Minutes quota immediately after your purchase of additional + minutes. + +##### What happens when my CI minutes run out + +When the CI minutes run out, an email is sent automatically to notify the owner(s) +of the group/namespace, including a link to [purchase more minutes](https://customers.gitlab.com/plans). + +If you are not the owner of the group, you will need to contact them to let them know they need to +[purchase more minutes](https://customers.gitlab.com/plans). + ## Subscription changes and your data When your subscription or trial expires, GitLab does not delete your data. diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index 0aef40262c9..df35638cba2 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -47,8 +47,8 @@ sudo service gitlab stop ### 3. Update Ruby -NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped -support for Ruby 2.4. Be sure to upgrade if necessary. +NOTE: Beginning in GitLab 12.2, we only support Ruby 2.6 or higher, and dropped +support for Ruby 2.5. Be sure to upgrade if necessary. You can check which version you are running with `ruby -v`. diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 7fbb4d84cfc..6faab685b26 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -4,6 +4,17 @@ type: reference # Account and limit settings +## Max attachment size + +You can change the maximum file size for attachments in comments and replies in GitLab. +Navigate to **Admin Area (wrench icon) > Settings > General**, then expand **Account and Limit**. +From here, you can increase or decrease by changing the value in `Maximum attachment size (MB)`. + +NOTE: **Note:** +If you choose a size larger than what is currently configured for the web server, +you will likely get errors. See the [troubleshooting section](#troubleshooting) for more +details. + ## Repository size limit **(STARTER)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/2016/09/22/gitlab-8-12-released/#limit-project-size-ee). @@ -51,14 +62,18 @@ For details on manually purging files, see [reducing the repository size using G NOTE: **Note:** GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit). -<!-- ## Troubleshooting +## Troubleshooting + +### 413 Request Entity Too Large + +If you are attaching a file to a comment or reply in GitLab and receive the `413 Request Entity Too Large` +error, it is likely caused by having a [max attachment size](#max-attachment-size) +larger than what the web server is configured to allow. -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. +If you wanted to increase the max attachment size to 200m in a GitLab +[Omnibus](https://docs.gitlab.com/omnibus/) install, for example, you might need to +add the line below to `/etc/gitlab/gitlab.rb` before increasing the max attachment size: -Each scenario can be a third-level heading, e.g. `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +``` +nginx['client_max_body_size'] = "200m" +``` diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 43640f1b16a..bd76b052422 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -94,52 +94,6 @@ a group in the **Usage Quotas** page available to the group page settings list. ![Group pipelines quota](img/group_pipelines_quota.png) -## Extra Shared Runners pipeline minutes quota **(FREE ONLY)** - -If you're using GitLab.com, you can purchase additional CI minutes so your -pipelines will not be blocked after you have used all your CI minutes from your -main quota. Additional minutes: - -- Are only used once the shared quota included in your subscription runs out. -- Roll over month to month. - -In order to purchase additional minutes, you should follow these steps: - -1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**. - - ![Buy additional minutes](img/buy_btn.png) - -1. Locate the subscription card that is linked to your group on GitLab.com, - click on **Buy more CI minutes**, and complete the details about the transaction. - - ![Buy additional minutes](img/buy_minutes_card.png) - -1. Once we have processed your payment, the extra CI minutes - will be synced to your Group and you can visualize it from the - **Group > Settings > Pipelines quota** page: - - ![Additional minutes](img/additional_minutes.png) - -Be aware that: - -1. If you have purchased extra CI minutes before the purchase of a paid plan, - we will calculate a pro-rated charge for your paid plan. That means you may - be charged for less than one year since your subscription was previously - created with the extra CI minutes. -1. Once the extra CI minutes has been assigned to a Group they cannot be transferred - to a different Group. -1. If you have some minutes used over your default quota, these minutes will - be deducted from your Additional Minutes quota immediately after your purchase of additional - minutes. - -## What happens when my CI minutes quota run out - -When the CI minutes quota run out, an email is sent automatically to notifies the owner(s) of the group/namespace which -includes a link to [purchase more minutes](https://customers.gitlab.com/plans). - -If you are not the owner of the group, you will need to contact them to let them know they need to -[purchase more minutes](https://customers.gitlab.com/plans). - ## Archive jobs **(CORE ONLY)** Archiving jobs is useful for reducing the CI/CD footprint on the system by diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md new file mode 100644 index 00000000000..2319e96bc23 --- /dev/null +++ b/doc/user/analytics/productivity_analytics.md @@ -0,0 +1,69 @@ +# Productivity Analytics **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2 (enabled by feature flags `analytics` and `productivity_analytics`). + +Track development velocity with Productivity Analytics. + +For many companies, the development cycle is a blackbox and getting an estimate of how +long, on average, it takes to deliver features is an enormous endeavor. + +While [Cycle Analytics](../project/cycle_analytics.md) focuses on the entire +SDLC process, Productivity Analytics provides a way for Engineering Management to +drill down in a systematic way to uncover patterns and causes for success or failure at +an individual, project or group level. + +Productivity can slow down for many reasons ranging from degrading code base to quickly +growing teams. In order to investigate, department or team leaders can start by visualizing the time +it takes for merge requests to be merged. + +## Supported features + +Productivity Analytics allows GitLab users to: + +- Visualize typical Merge Request lifetime and statistics. Use a histogram + that shows the distribution of the time elapsed between creating and merging + Merge Requests. +- Drill down into the most time consuming Merge Requests, select a number of outliers, + and filter down all subsequent charts to investigate potential causes. +- Filter by group, project, author, label, milestone, or a specific date range. + Filter down, for example, to the Merge Requests of a specific author in a group + or project during a milestone or specific date range. + +## Accessing metrics and visualizations + +To access the **Productivity Analytics** page, go to **Analytics > Productivity Analytics**. + +The following metrics and visualizations are available on a project or group level: + +- Histogram showing the number of Merge Request that took a specified number of days to + merge after creation. Select a specific column to filter down subsequent charts. +- Histogram showing a breakdown of the time taken (in hours) to merge a Merge Request. + The following intervals are available: + - Time from first commit to first comment. + - Time from first comment until last commit. + - Time from last commit to merge. +- Histogram showing the size or complexity of a Merge Request, using the following: + - Number of commits per Merge Request. + - Number of lines of code per commit. + - Number of files touched. +- Table showing list of Merge Requests with their respective times and size metrics. + Can be sorted by the above metrics. + - Users can sort by any of the above metrics + +## Retrieving data + +Users can retrieve three months of data when they deploy Cycle Analytics for the first time. + +To retrieve data for a different time span, run the following in the GitLab directory: + +```sh +MERGED_AT_AFTER = <your_date> rake gitlab:productivity_analytics:recalc +``` + +## Permissions + +The **Productivity Analytics** dashboard can be accessed only: + +- On GitLab instances and namespaces on + [Premium or Silver tier](https://about.gitlab.com/pricing/) and above. +- By users with [Reporter access](../permissions.md) and above. diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 971fd3f28c4..c9fbd7effa0 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -81,13 +81,12 @@ IP based firewall can be configured by looking up all ## Shared Runners -Shared Runners on GitLab.com run in [autoscale mode] and powered by -Google Cloud Platform. Autoscaling means reduced -waiting times to spin up CI/CD jobs, and isolated VMs for each project, -thus maximizing security. -They're free to use for public open source projects and limited to 2000 CI -minutes per month per group for private projects. Read about all -[GitLab.com plans](https://about.gitlab.com/pricing/). +Shared Runners on GitLab.com run in [autoscale mode] and powered by Google Cloud Platform. +Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project, +thus maximizing security. They're free to use for public open source projects and limited +to 2000 CI minutes per month per group for private projects. More minutes +[can be purchased](../../subscriptions/index.md#extra-shared-runners-pipeline-minutes), if +needed. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/). All your CI/CD jobs run on [n1-standard-1 instances](https://cloud.google.com/compute/docs/machine-types) with 3.75GB of RAM, CoreOS and the latest Docker Engine installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default diff --git a/doc/user/markdown.md b/doc/user/markdown.md index edb1e904f2b..6cfc8b6429b 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -533,6 +533,10 @@ This snippet links to `<wiki_root>/miscellaneous.md`: [Link to Related Page](/miscellaneous.md) ``` +### Embedding metrics in GitLab Flavored Markdown + +Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details. + ## Standard markdown and extensions in GitLab All standard markdown formatting should work as expected within GitLab. Some standard diff --git a/doc/user/permissions.md b/doc/user/permissions.md index d92435ef724..16684b9f72b 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -47,6 +47,7 @@ The following table depicts the various user permission levels in a project. | View approved/blacklisted licenses **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | | View license management reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View Security reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | +| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | @@ -93,7 +94,7 @@ The following table depicts the various user permission levels in a project. | Remove a container registry image | | | ✓ | ✓ | ✓ | | Create/edit/delete project milestones | | | ✓ | ✓ | ✓ | | Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ | -| View dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ | +| View vulnerabilities in Dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Create issue from vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Dismiss vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Apply code change suggestions | | | ✓ | ✓ | ✓ | @@ -232,6 +233,16 @@ nested groups if you have membership in one of its parents. To learn more, read through the documentation on [subgroups memberships](group/subgroups/index.md#membership). +## Guest User + +Create a user and assign to a project with a role as `Guest` user, this user +will be considered as guest user by GitLab and will not take up the license. +There is no specific `Guest` role for newly created users. If this user will +be assigned a higher role to any of the projects and groups then this user will +take a license seat. If a user creates a project this user becomes a maintainer, +therefore, takes up a license seat as well, in order to prevent this you have +to go and edit user profile and mark the user as External. + ## External users permissions In cases where it is desired that a user has access only to some internal or diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 248188a6457..d556daa3460 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -44,7 +44,7 @@ the following table. | Scope | Introduced in | Description | | ------------------ | ------------- | ----------- | | `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed. | -| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete access to the API and Container Registry (read/write). | +| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. | | `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) | Allows to read (pull) [container registry] images if a project is private and authorization is required. | | `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an admin). | | `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894) | Allows read-only access (pull) to the repository through git clone. | diff --git a/doc/user/project/integrations/img/embed_metrics.png b/doc/user/project/integrations/img/embed_metrics.png Binary files differnew file mode 100644 index 00000000000..6f9660c9aec --- /dev/null +++ b/doc/user/project/integrations/img/embed_metrics.png diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index ea58a08e127..6e0f39956d3 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -14,7 +14,7 @@ To enable Mattermost integration you must create an incoming webhook integration 1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable -it on `https://mattermost.example/admin_console/integrations/custom`. +it on **Mattermost System Console > Integrations > Integration Management**, or on **Mattermost System Console > Integrations > Custom Integrations** in Mattermost versions 5.11 and earlier. Display name override is not enabled by default, you need to ask your admin to enable it on that same section. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 44439b59e77..aa7db97c413 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -354,6 +354,27 @@ Prometheus server. ![Merge Request with Performance Impact](img/merge_request_performance.png) +## Embedding metric charts within Gitlab Flavored Markdown + +> [Introduced][ce-29691] in GitLab 12.2. +> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics. + +It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm). + +To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`. + +The following requirements must be met for the metric to unfurl: + +- The `<environment_id>` must correspond to a real environment. +- Prometheus must be monitoring the environment. +- The GitLab instance must be configured to receive data from the environment. +- The user must be allowed access to the monitoring dashboard for the environment ([Reporter or higher](../../permissions.md)). +- The dashboard must have data within the last 8 hours. + + If all of the above are true, then the metric will unfurl as seen below: + +![Embedded Metrics](img/embed_metrics.png) + ## Troubleshooting If the "No data found" screen continues to appear, it could be due to: @@ -376,4 +397,5 @@ If the "No data found" screen continues to appear, it could be due to: [ci-environment-slug]: ../../../ci/variables/#predefined-environment-variables [ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935 [ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408 +[ce-29691]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29691 [promgldocs]: ../../../administration/monitoring/prometheus/index.md diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index 7b031f83cb1..d7d168710ef 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -50,7 +50,12 @@ The button to do this has a different label depending on whether the issue is al #### 3. Assignee -An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER). +An issue can be assigned to: + +- Yourself. +- Another person. +- [Many people](#31-multiple-assignees-STARTER). **(STARTER)** + The assignee(s) can be changed as often as needed. The idea is that the assignees are responsible for that issue until it's reassigned to someone else to take it from there. When assigned to someone, it will appear in their assigned issues list. diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 7ff30d1b813..7637e30dfb4 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -287,6 +287,8 @@ as pushing changes: - Set the target of the merge request to a particular branch. - Set the merge request to merge when its pipeline succeeds. - Set the merge request to remove the source branch when it's merged. +- Set the title of the merge request to a particular title. +- Set the description of the merge request to a particular description. ### Create a new merge request using git push options diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 509576c8d6f..5161b25de99 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -229,12 +229,6 @@ The default approval settings can now be overridden when creating a in the **No. approvals required** box. 1. Click **Update approvers**. -There are however some restrictions: - -- The amount of required approvals, if changed, must be greater than the default - set at the project level. This ensures that you're not forced to adjust settings - when someone is unavailable for approval, yet the process is still enforced. - NOTE: **Note:** If you are contributing to a forked project, things are a little different. Read what happens when the @@ -242,14 +236,9 @@ Read what happens when the ## Overriding merge request approvals default settings **(PREMIUM)** -In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), and -**Can override approvers and approvals required per merge request** is checked, there are a few more -restrictions (compared to [GitLab Starter](#overriding-the-merge-request-approvals-default-settings)): - -- Approval rules can be added to an MR with no restriction. -- For project sourced approval rules, editing and removing approvers is not allowed. -- The approvals required of all approval rules is configurable, but if a rule is backed by a project rule, then it is restricted - to the minimum approvals required set in the project's corresponding rule. +In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), +and **Can override approvers and approvals required per merge request** is checked, +approval rules can be added to an MR with no restriction. ## Resetting approvals on push diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 4e25d8545e9..f3e9c950efd 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -6,7 +6,9 @@ type: reference, howto > - Introduced in GitLab 9.1 as [Trigger Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533). > - [Renamed to Pipeline Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853) in GitLab 9.2. -> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). + +NOTE: **Note:** +Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). Pipelines are normally run based on certain conditions being met. For example, when a branch is pushed to repository. diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 196ef1fcdfa..c36ee5af63f 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -125,6 +125,12 @@ module API optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 + optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' + given snowplow_enabled: ->(val) { val } do + requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' + optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain' + optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic' + end ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb index c83cec9dc4a..45af30f46dc 100644 --- a/lib/expand_variables.rb +++ b/lib/expand_variables.rb @@ -3,6 +3,20 @@ module ExpandVariables class << self def expand(value, variables) + variables_hash = nil + + value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do + variables_hash ||= transform_variables(variables) + variables_hash[$1 || $2] + end + end + + private + + def transform_variables(variables) + # Lazily initialise variables + variables = variables.call if variables.is_a?(Proc) + # Convert hash array to variables if variables.is_a?(Array) variables = variables.reduce({}) do |hash, variable| @@ -11,9 +25,7 @@ module ExpandVariables end end - value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do - variables[$1 || $2] - end + variables end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 2fd76bc3690..29a52b9da17 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -16,8 +16,11 @@ module Gitlab dependencies needs before_script after_script variables environment coverage retry parallel extends].freeze + REQUIRED_BY_NEEDS = %i[stage].freeze + validations do validates :config, allowed_keys: ALLOWED_KEYS + validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs? validates :config, presence: true validates :script, presence: true validates :name, presence: true diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 0405292a25b..65029f5ce7f 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -23,12 +23,17 @@ module Gitlab @command.seeds_block&.call(pipeline) ## - # Populate pipeline with all stages, and stages with builds. + # Gather all runtime build/stage errors # - pipeline.stage_seeds.each do |stage| - pipeline.stages << stage.to_resource + if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence + return error(seeds_errors.join("\n")) end + ## + # Populate pipeline with all stages, and stages with builds. + # + pipeline.stages = pipeline.stage_seeds.map(&:to_resource) + if pipeline.stages.none? return error('No stages / jobs for this pipeline.') end diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb index 1fd3a61017f..e9e22569ae0 100644 --- a/lib/gitlab/ci/pipeline/seed/base.rb +++ b/lib/gitlab/ci/pipeline/seed/base.rb @@ -13,6 +13,10 @@ module Gitlab raise NotImplementedError end + def errors + raise NotImplementedError + end + def to_resource raise NotImplementedError end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index ab0d4c38ab6..b0ce7457926 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -9,10 +9,15 @@ module Gitlab delegate :dig, to: :@attributes + # When the `ci_dag_limit_needs` is enabled it uses the lower limit + LOW_NEEDS_LIMIT = 5 + HARD_NEEDS_LIMIT = 50 + def initialize(pipeline, attributes, previous_stages) @pipeline = pipeline @attributes = attributes @previous_stages = previous_stages + @needs_attributes = dig(:needs_attributes) @only = Gitlab::Ci::Build::Policy .fabricate(attributes.delete(:only)) @@ -27,8 +32,15 @@ module Gitlab def included? strong_memoize(:inclusion) do all_of_only? && - none_of_except? && - all_of_needs? + none_of_except? + end + end + + def errors + return unless included? + + strong_memoize(:errors) do + needs_errors end end @@ -48,6 +60,18 @@ module Gitlab @attributes.to_h.dig(:options, :trigger).present? end + def to_resource + strong_memoize(:resource) do + if bridge? + ::Ci::Bridge.new(attributes) + else + ::Ci::Build.new(attributes) + end + end + end + + private + def all_of_only? @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } end @@ -56,24 +80,30 @@ module Gitlab @except.none? { |spec| spec.satisfied_by?(@pipeline, self) } end - def all_of_needs? - return true unless Feature.enabled?(:ci_dag_support, @pipeline.project) - return true if dig(:needs_attributes).nil? + def needs_errors + return if @needs_attributes.nil? + + if @needs_attributes.size > max_needs_allowed + return [ + "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \ + "See needs keyword documentation for more details" + ] + end - dig(:needs_attributes).all? do |need| - @previous_stages.any? do |stage| + @needs_attributes.flat_map do |need| + result = @previous_stages.any? do |stage| stage.seeds_names.include?(need[:name]) end - end + + "#{name}: needs '#{need[:name]}'" unless result + end.compact end - def to_resource - strong_memoize(:resource) do - if bridge? - ::Ci::Bridge.new(attributes) - else - ::Ci::Build.new(attributes) - end + def max_needs_allowed + if Feature.enabled?(:ci_dag_limit_needs, @project, default_enabled: true) + LOW_NEEDS_LIMIT + else + HARD_NEEDS_LIMIT end end end diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index 7c737027445..b600df2f656 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -33,6 +33,12 @@ module Gitlab end end + def errors + strong_memoize(:errors) do + seeds.flat_map(&:errors).compact + end + end + def seeds_names strong_memoize(:seeds_names) do seeds.map(&:name).to_set diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb index 560fe63df0e..87bd257f69a 100644 --- a/lib/gitlab/config/entry/attributable.rb +++ b/lib/gitlab/config/entry/attributable.rb @@ -18,6 +18,10 @@ module Gitlab config[attribute] end + + define_method("has_#{attribute}?") do + config.is_a?(Hash) && config.key?(attribute) + end end end end diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 6796fcce75f..0289e675c6b 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -26,6 +26,17 @@ module Gitlab end end + class RequiredKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + present_keys = options[:in] - value.try(:keys).to_a + + if present_keys.any? + record.errors.add(attribute, "missing required keys: " + + present_keys.join(', ')) + end + end + end + class AllowedValuesValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless options[:in].include?(value.to_s) diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 40bda3410e1..37fadb47736 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -60,7 +60,8 @@ module Gitlab # rubocop:disable Metrics/ParameterLists def build( project:, user:, ref:, oldrev: nil, newrev: nil, - commits: [], commits_count: nil, message: nil, push_options: {}) + commits: [], commits_count: nil, message: nil, push_options: {}, + with_changed_files: true) commits = Array(commits) @@ -75,7 +76,7 @@ module Gitlab # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259 commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do commits_limited.map do |commit| - commit.hook_attrs(with_changed_files: true) + commit.hook_attrs(with_changed_files: with_changed_files) end end diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index d98b85fecc4..24d752b8a4b 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -27,6 +27,18 @@ module Gitlab end end + def includes_branches? + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.branch_ref?(ref) + end + end + + def includes_tags? + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.tag_ref?(ref) + end + end + private def deserialize_changes(changes) diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 42c4745ff98..6e4286589c1 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -3,8 +3,8 @@ module Gitlab module Kubernetes module Helm - HELM_VERSION = '2.12.3'.freeze - KUBECTL_VERSION = '1.11.7'.freeze + HELM_VERSION = '2.14.3'.freeze + KUBECTL_VERSION = '1.11.10'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze diff --git a/lib/gitlab/kubernetes/helm/reset_command.rb b/lib/gitlab/kubernetes/helm/reset_command.rb index 37e1d8573ab..a35ffa34c58 100644 --- a/lib/gitlab/kubernetes/helm/reset_command.rb +++ b/lib/gitlab/kubernetes/helm/reset_command.rb @@ -38,9 +38,9 @@ module Gitlab # Tracking this method to be removed here: # https://gitlab.com/gitlab-org/gitlab-ce/issues/52791#note_199374155 def delete_tiller_replicaset - command = %w[kubectl delete replicaset -n gitlab-managed-apps -l name=tiller] + delete_args = %w[replicaset -n gitlab-managed-apps -l name=tiller] - command.shelljoin + Gitlab::Kubernetes::KubectlCmd.delete(*delete_args) end def reset_helm_command diff --git a/lib/gitlab/kubernetes/kubectl_cmd.rb b/lib/gitlab/kubernetes/kubectl_cmd.rb new file mode 100644 index 00000000000..981eb5681dc --- /dev/null +++ b/lib/gitlab/kubernetes/kubectl_cmd.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + module KubectlCmd + class << self + def delete(*args) + %w(kubectl delete).concat(args).shelljoin + end + + def apply_file(filename, *args) + raise ArgumentError, "filename is not present" unless filename.present? + + %w(kubectl apply -f).concat([filename], args).shelljoin + end + end + end + end +end diff --git a/lib/gitlab/snowplow_tracker.rb b/lib/gitlab/snowplow_tracker.rb new file mode 100644 index 00000000000..9f12513e09e --- /dev/null +++ b/lib/gitlab/snowplow_tracker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'snowplow-tracker' + +module Gitlab + module SnowplowTracker + NAMESPACE = 'cf' + + class << self + def track_event(category, action, label: nil, property: nil, value: nil, context: nil) + tracker&.track_struct_event(category, action, label, property, value, context, Time.now.to_i) + end + + private + + def tracker + return unless enabled? + + @tracker ||= ::SnowplowTracker::Tracker.new(emitter, subject, NAMESPACE, Gitlab::CurrentSettings.snowplow_site_id) + end + + def subject + ::SnowplowTracker::Subject.new + end + + def emitter + ::SnowplowTracker::Emitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname) + end + + def enabled? + Gitlab::CurrentSettings.snowplow_enabled? + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 7e3a695e52a..038553c5dd7 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -142,7 +142,9 @@ module Gitlab Gitlab::UsageDataCounters::WebIdeCounter, Gitlab::UsageDataCounters::NoteCounter, Gitlab::UsageDataCounters::SnippetCounter, - Gitlab::UsageDataCounters::SearchCounter + Gitlab::UsageDataCounters::SearchCounter, + Gitlab::UsageDataCounters::CycleAnalyticsCounter, + Gitlab::UsageDataCounters::SourceCodeCounter ] end diff --git a/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb new file mode 100644 index 00000000000..1ff4296ef65 --- /dev/null +++ b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class CycleAnalyticsCounter < BaseCounter + KNOWN_EVENTS = %w[views].freeze + PREFIX = 'cycle_analytics' + end +end diff --git a/lib/gitlab/usage_data_counters/source_code_counter.rb b/lib/gitlab/usage_data_counters/source_code_counter.rb new file mode 100644 index 00000000000..8a1771a7bd1 --- /dev/null +++ b/lib/gitlab/usage_data_counters/source_code_counter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class SourceCodeCounter < BaseCounter + KNOWN_EVENTS = %w[pushes].freeze + PREFIX = 'source_code' + end +end diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake index 56b81106c5f..4ec4fdd281f 100644 --- a/lib/tasks/services.rake +++ b/lib/tasks/services.rake @@ -86,7 +86,7 @@ namespace :services do doc_start = Time.now doc_path = File.join(Rails.root, 'doc', 'api', 'services.md') - result = ERB.new(services_template, 0, '>') + result = ERB.new(services_template, trim_mode: '>') .result(OpenStruct.new(services: services).instance_eval { binding }) File.open(doc_path, 'w') do |f| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d33c62031c4..dd69fa1f8f6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2953,6 +2953,9 @@ msgstr "" msgid "Collapse sidebar" msgstr "" +msgid "Collector hostname" +msgstr "" + msgid "ComboSearch is not defined" msgstr "" @@ -3120,6 +3123,9 @@ msgstr "" msgid "Configure storage path settings." msgstr "" +msgid "Configure the %{link} integration." +msgstr "" + msgid "Configure the way a user creates a new account." msgstr "" @@ -3261,6 +3267,9 @@ msgstr "" msgid "ConvDev Index" msgstr "" +msgid "Cookie domain" +msgstr "" + msgid "Copied" msgstr "" @@ -4253,6 +4262,9 @@ msgstr "" msgid "Enable shared Runners" msgstr "" +msgid "Enable snowplow tracking" +msgstr "" + msgid "Enable two-factor authentication" msgstr "" @@ -5065,6 +5077,9 @@ msgstr "" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgstr "" +msgid "Forgot your password?" +msgstr "" + msgid "Fork" msgstr "" @@ -5215,6 +5230,9 @@ msgstr "" msgid "GitLab User" msgstr "" +msgid "GitLab member or Email address" +msgstr "" + msgid "GitLab project export" msgstr "" @@ -6419,9 +6437,15 @@ msgstr "" msgid "Last seen" msgstr "" +msgid "Last successful update" +msgstr "" + msgid "Last update" msgstr "" +msgid "Last update attempt" +msgstr "" + msgid "Last updated" msgstr "" @@ -9906,9 +9930,6 @@ msgstr "" msgid "Select branch/tag" msgstr "" -msgid "Select members to invite" -msgstr "" - msgid "Select merge moment" msgstr "" @@ -10280,6 +10301,9 @@ msgstr "" msgid "Similar issues" msgstr "" +msgid "Site ID" +msgstr "" + msgid "Size and domain settings for static websites" msgstr "" @@ -10310,6 +10334,9 @@ msgstr "" msgid "SnippetsEmptyState|They can be either public or private." msgstr "" +msgid "Snowplow" +msgstr "" + msgid "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." msgstr "" @@ -12503,6 +12530,9 @@ msgstr "" msgid "Username is available." msgstr "" +msgid "Username or email" +msgstr "" + msgid "Users" msgstr "" diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index c2b0482d789..94245bbfcba 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -52,13 +52,11 @@ module QA raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given? if Runtime::User.ldap_user? - sign_in_using_ldap_credentials + sign_in_using_ldap_credentials(user || Runtime::User) else sign_in_using_gitlab_credentials(user || Runtime::User) end end - - Page::Main::Menu.perform(&:has_personal_area?) end def sign_in_using_admin_credentials @@ -76,6 +74,25 @@ module QA Page::Main::Menu.perform(&:has_personal_area?) end + def sign_in_using_ldap_credentials(user) + # Log out if already logged in + Page::Main::Menu.perform do |menu| + menu.sign_out if menu.has_personal_area?(wait: 0) + end + + using_wait_time 0 do + set_initial_password_if_present + + switch_to_ldap_tab + + fill_element :username_field, user.ldap_username + fill_element :password_field, user.ldap_password + click_element :sign_in_button + end + + Page::Main::Menu.perform(&:has_personal_area?) + end + def self.path '/users/sign_in' end @@ -133,14 +150,6 @@ module QA private - def sign_in_using_ldap_credentials - switch_to_ldap_tab - - fill_element :username_field, Runtime::User.ldap_username - fill_element :password_field, Runtime::User.ldap_password - click_element :sign_in_button - end - def sign_in_using_gitlab_credentials(user) switch_to_sign_in_tab if has_sign_in_tab? switch_to_standard_tab if has_standard_tab? diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb index 0d138417176..65c02801d67 100644 --- a/qa/qa/page/project/issue/new.rb +++ b/qa/qa/page/project/issue/new.rb @@ -6,7 +6,7 @@ module QA module Issue class New < Page::Base view 'app/views/shared/issuable/_form.html.haml' do - element :submit_issue_button, 'form.submit "Submit' # rubocop:disable QA/ElementWithPattern + element :issuable_create_button end view 'app/views/shared/issuable/form/_title.html.haml' do @@ -26,7 +26,7 @@ module QA end def create_new_issue - click_on 'Submit issue' + click_element :issuable_create_button, Page::Project::Issue::Show end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 507dccb52d0..45dad9bc0ae 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -14,7 +14,7 @@ module QA end view 'app/assets/javascripts/notes/components/discussion_filter.vue' do - element :discussion_filter + element :discussion_filter, required: true element :filter_options end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 3fe048f752a..838d59b59cb 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -5,7 +5,7 @@ module QA module Project class Menu < Page::Base include SubMenus::Common - + include SubMenus::Project include SubMenus::CiCd include SubMenus::Issues include SubMenus::Operations diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 7969de726e4..45ab2396a04 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -5,7 +5,8 @@ require 'securerandom' module QA module Resource class MergeRequest < Base - attr_accessor :title, + attr_accessor :id, + :title, :description, :source_branch, :target_branch, @@ -74,6 +75,28 @@ module QA page.create_merge_request end end + + def fabricate_via_api! + populate(:target, :source) + super + end + + def api_get_path + "/projects/#{project.id}/merge_requests/#{id}" + end + + def api_post_path + "/projects/#{project.id}/merge_requests" + end + + def api_post_body + { + description: @description, + source_branch: @source_branch, + target_branch: @target_branch, + title: @title + } + end end end end diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb index b4d70fc191a..632a0f5f2a9 100644 --- a/qa/qa/scenario/test/sanity/selectors.rb +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -7,11 +7,13 @@ module QA class Selectors < Scenario::Template include Scenario::Bootable - PAGES = [QA::Page].freeze + def pages + @pages ||= [QA::Page] + end def perform(*) - validators = PAGES.map do |pages| - Page::Validator.new(pages) + validators = pages.map do |page| + Page::Validator.new(page) end validators.flat_map(&:errors).tap do |errors| diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb index b54fd5628f2..c5cddff56cd 100644 --- a/qa/qa/service/omnibus.rb +++ b/qa/qa/service/omnibus.rb @@ -11,11 +11,12 @@ module QA end def gitlab_ctl(command, input: nil) - if input.nil? - shell "docker exec #{@name} gitlab-ctl #{command}" - else - shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" - end + docker_exec("gitlab-ctl #{command}", input: input) + end + + def docker_exec(command, input: nil) + command = "#{input} | #{command}" if input + shell "docker exec #{@name} bash -c '#{command}'" end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index 9e48ee7ca2a..891cef6c420 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -7,13 +7,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - @merge_request = Resource::MergeRequest.fabricate! do |merge_request| + project = Resource::Project.fabricate_via_api! do |project| + project.name = 'project' + end + + @merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.project = project merge_request.title = 'This is a merge request' - merge_request.description = 'For downloading patches and diffs' + merge_request.description = '... for downloading patches and diffs' end end - it 'user views merge request email patches' do + it 'views the merge request email patches' do @merge_request.visit! Page::MergeRequest::Show.perform(&:view_email_patches) @@ -22,7 +27,7 @@ module QA expect(page).to have_content('diff --git a/added_file.txt b/added_file.txt') end - it 'user views merge request plain diff' do + it 'views the merge request plain diff' do @merge_request.visit! Page::MergeRequest::Show.perform(&:view_plain_diff) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 2fe4e4d9d1f..f6411d8c5ad 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -3,36 +3,29 @@ require 'digest/sha1' module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/70 - context 'Release', :docker, :quarantine do + context 'Release', :docker do describe 'Git clone using a deploy key' do - def login + before do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - end - - before(:all) do - login @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate_via_browser_ui! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] resource.image = 'gitlab/gitlab-runner:ubuntu' end - - Page::Main::Menu.perform(&:sign_out) end - after(:all) do + after do Service::Runner.new(@runner_name).remove! end @@ -46,9 +39,7 @@ module QA it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do key = key_class.new(*bits) - login - - Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate_via_browser_ui! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -56,7 +47,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate_via_browser_ui! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb index 9c1f9904a7a..1b8c926532a 100644 --- a/qa/qa/vendor/saml_idp/page/login.rb +++ b/qa/qa/vendor/saml_idp/page/login.rb @@ -12,6 +12,14 @@ module QA fill_in 'password', with: 'user1pass' click_on 'Login' end + + def login_if_required + login if login_required? + end + + def login_required? + page.has_text?('Enter your username and password') + end end end end diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index bf3ebce0cfe..2f9e4958ae1 100644 --- a/qa/spec/resource/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -19,7 +19,11 @@ describe QA::Resource::Repository::Push do expect { subject.files = [] }.to raise_error(ArgumentError) end - it 'does not raise if files is an array' do + it 'raises an error if files is not an array of hashes with :name and :content keys' do + expect { subject.files = [{ foo: 'foo' }] }.to raise_error(ArgumentError) + end + + it 'does not raise if files is an array of hashes with :name and :content keys' do expect { subject.files = files }.not_to raise_error end end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index caf96a213e1..340831aa06d 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -192,6 +192,30 @@ describe QA::Runtime::Env do end end + describe '.knapsack?' do + it 'returns true if KNAPSACK_GENERATE_REPORT is defined' do + stub_env('KNAPSACK_GENERATE_REPORT', 'true') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns true if KNAPSACK_REPORT_PATH is defined' do + stub_env('KNAPSACK_REPORT_PATH', '/a/path') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns true if KNAPSACK_TEST_FILE_PATTERN is defined' do + stub_env('KNAPSACK_TEST_FILE_PATTERN', '/a/**/pattern') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns false if neither KNAPSACK_GENERATE_REPORT nor KNAPSACK_REPORT_PATH nor KNAPSACK_TEST_FILE_PATTERN are defined' do + expect(described_class.knapsack?).to be_falsey + end + end + describe '.require_github_access_token!' do it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do stub_env('GITHUB_ACCESS_TOKEN', nil) diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 84bbe0525e5..c3a5abf2b7a 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -15,7 +15,7 @@ describe 'bin/changelog' do allow(entry).to receive(:branch_name).and_return('long-branch-' * 100) file_path = entry.send(:file_path) - expect(file_path.length).to eq(140) + expect(file_path.length).to eq(99) end end diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb index 2dc97e18113..5e6ceef2517 100644 --- a/spec/controllers/projects/cycle_analytics_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb @@ -11,6 +11,20 @@ describe Projects::CycleAnalyticsController do project.add_maintainer(user) end + context "counting page views for 'show'" do + it 'increases the counter' do + expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views) + + get(:show, + params: { + namespace_id: project.namespace, + project_id: project + }) + + expect(response).to be_success + end + end + describe 'cycle analytics not set up flag' do context 'with no data' do it 'is true' do diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 8ee3168273f..b958f419a19 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -60,7 +60,7 @@ describe Projects::RawController do execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, file_path)) + expect(response).to have_gitlab_http_status(429) end it 'logs the event on auth.log' do @@ -92,7 +92,7 @@ describe Projects::RawController do execute_raw_requests(requests: 3, project: project, file_path: modified_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, modified_path)) + expect(response).to have_gitlab_http_status(429) end end @@ -120,7 +120,7 @@ describe Projects::RawController do execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, file_path)) + expect(response).to have_gitlab_http_status(429) # Accessing upcase version of readme file_path = "#{commit_sha}/README.md" diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index faf3c990cb2..d05482f095e 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' describe RegistrationsController do include TermsHelper + before do + stub_feature_flags(invisible_captcha: false) + end + describe '#create' do let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } let(:user_params) { { user: base_user_params } } @@ -88,6 +92,88 @@ describe RegistrationsController do end end + context 'when invisible captcha is enabled' do + before do + stub_feature_flags(invisible_captcha: true) + InvisibleCaptcha.timestamp_threshold = treshold + end + + let(:treshold) { 4 } + let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } } + let(:form_rendered_time) { Time.current } + let(:submit_time) { form_rendered_time + treshold } + let(:auth_log_attributes) do + { + message: auth_log_message, + env: :invisible_captcha_signup_bot_detected, + ip: '0.0.0.0', + request_method: 'POST', + fullpath: '/users' + } + end + + describe 'the honeypot has not been filled and the signup form has not been submitted too quickly' do + it 'creates an account' do + travel_to(submit_time) do + expect { post(:create, params: user_params, session: session_params) }.to change(User, :count).by(1) + end + end + end + + describe 'honeypot spam detection' do + let(:user_params) { super().merge(firstname: 'Roy', lastname: 'Batty') } + let(:auth_log_message) { 'Invisible_Captcha_Honeypot_Request' } + + it 'logs the request, refuses to create an account and renders an empty body' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_honeypot, 'Counter of blocked sign up attempts with filled honeypot') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to have_gitlab_http_status(200) + expect(response.body).to be_empty + end + end + end + + describe 'timestamp spam detection' do + let(:auth_log_message) { 'Invisible_Captcha_Timestamp_Request' } + + context 'the sign up form has been submitted without the invisible_captcha_timestamp parameter' do + let(:session_params) { nil } + + it 'logs the request, refuses to create an account and displays a flash alert' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.' + end + end + end + + context 'the sign up form has been submitted too quickly' do + let(:submit_time) { form_rendered_time } + + it 'logs the request, refuses to create an account and displays a flash alert' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.' + end + end + end + end + end + context 'when terms are enforced' do before do enforce_terms diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 855cf22642e..832c4a57aa3 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -10,6 +10,7 @@ describe 'Invites' do let(:group_invite) { group.group_members.invite.last } before do + stub_feature_flags(invisible_captcha: false) project.add_maintainer(owner) group.add_user(owner, Gitlab::Access::OWNER) group.add_developer('user@example.com', owner) diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 5e52c82a234..4dbdea02e27 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -38,7 +38,7 @@ describe 'User visits the profile preferences page' do describe 'User changes their default dashboard', :js do it 'creates a flash message' do - select 'Starred Projects', from: 'user_dashboard' + select2('stars', from: '#user_dashboard') click_button 'Save' wait_for_requests @@ -47,7 +47,7 @@ describe 'User visits the profile preferences page' do end it 'updates their preference' do - select 'Starred Projects', from: 'user_dashboard' + select2('stars', from: '#user_dashboard') click_button 'Save' wait_for_requests diff --git a/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb new file mode 100644 index 00000000000..6d587053b4f --- /dev/null +++ b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Projects > Raw > User interacts with raw endpoint' do + include RepoHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + let(:file_path) { 'master/README.md' } + + before do + stub_application_setting(raw_blob_request_limit: 3) + project.add_developer(user) + create_file_in_repo(project, 'master', 'master', 'README.md', 'readme content') + + sign_in(user) + end + + context 'when user access a raw file' do + it 'renders the page successfully' do + visit project_raw_url(project, file_path) + + expect(source).to eq('') # Body is filled in by gitlab-workhorse + end + end + + context 'when user goes over the rate requests limit' do + it 'returns too many requests' do + 4.times do + visit project_raw_url(project, file_path) + end + + expect(source).to have_content('You are being redirected') + click_link('redirected') + expect(page).to have_content('You cannot access the raw file. Please wait a minute.') + end + end +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index dac8c8e7a29..1d8c9e7e426 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -95,6 +95,42 @@ describe 'Login' do end end + describe 'with an unconfirmed email address' do + let!(:user) { create(:user, confirmed_at: nil) } + let(:grace_period) { 2.days } + + before do + stub_application_setting(send_user_confirmation_email: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period + end + + context 'within the grace period' do + it 'allows to login' do + expect(authentication_metrics).to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(page).not_to have_content('You have to confirm your email address before continuing.') + expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path) + end + end + + context 'when the confirmation grace period is expired' do + it 'prevents the user from logging in and renders a resend confirmation email link' do + travel_to((grace_period + 1.day).from_now) do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + .and increment(:user_session_destroyed_counter).twice + + gitlab_sign_in(user) + + expect(page).to have_content('You have to confirm your email address before continuing.') + expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path) + end + end + end + end + describe 'with the ghost user' do it 'disallows login' do expect(authentication_metrics) diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index f5897bffaf0..cf57fafc4f5 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' describe 'Signup' do include TermsHelper + before do + stub_feature_flags(invisible_captcha: false) + end + let(:new_user) { build_stubbed(:user) } describe 'username validation', :js do diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb new file mode 100644 index 00000000000..647771ace92 --- /dev/null +++ b/spec/helpers/sessions_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SessionsHelper do + describe '#unconfirmed_email?' do + it 'returns true when the flash alert contains a devise failure unconfirmed message' do + flash[:alert] = t(:unconfirmed, scope: [:devise, :failure]) + expect(helper.unconfirmed_email?).to be_truthy + end + + it 'returns false when the flash alert does not contain a devise failure unconfirmed message' do + flash[:alert] = 'something else' + expect(helper.unconfirmed_email?).to be_falsey + end + end +end diff --git a/spec/helpers/tracking_helper_spec.rb b/spec/helpers/tracking_helper_spec.rb index 71505e8ea69..b0c98be4130 100644 --- a/spec/helpers/tracking_helper_spec.rb +++ b/spec/helpers/tracking_helper_spec.rb @@ -4,8 +4,32 @@ require 'spec_helper' describe TrackingHelper do describe '#tracking_attrs' do - it 'returns an empty hash' do - expect(helper.tracking_attrs('a', 'b', 'c')).to eq({}) + using RSpec::Parameterized::TableSyntax + + let(:input) { %w(a b c) } + let(:results) do + { + no_data: {}, + with_data: { data: { track_label: 'a', track_event: 'b', track_property: 'c' } } + } + end + + where(:snowplow_enabled, :environment, :result) do + true | 'production' | :with_data + false | 'production' | :no_data + true | 'development' | :no_data + false | 'development' | :no_data + true | 'test' | :no_data + false | 'test' | :no_data + end + + with_them do + it 'returns a hash' do + stub_application_setting(snowplow_enabled: snowplow_enabled) + allow(Rails).to receive(:env).and_return(environment.inquiry) + + expect(helper.tracking_attrs(*input)).to eq(results[result]) + end end end end diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index 4541119dd2e..57f99a09002 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -24,7 +24,6 @@ describe('Area component', () => { store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; areaChart = shallowMount(Area, { @@ -109,16 +108,6 @@ describe('Area component', () => { }); }); - describe('when exportMetricsToCsvEnabled is disabled', () => { - beforeEach(() => { - store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); - }); - - it('does not render the Download CSV button', () => { - expect(areaChart.contains('glbutton-stub')).toBe(false); - }); - }); - describe('methods', () => { describe('formatTooltipText', () => { const mockDate = deploymentData[0].created_at; @@ -264,23 +253,5 @@ describe('Area component', () => { expect(areaChart.vm.yAxisLabel).toBe('CPU'); }); }); - - describe('csvText', () => { - it('converts data from json to csv', () => { - const header = `timestamp,${mockGraphData.y_label}`; - const data = mockGraphData.queries[0].result[0].values; - const firstRow = `${data[0][0]},${data[0][1]}`; - - expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); - }); - }); - - describe('downloadLink', () => { - it('produces a link to download metrics as csv', () => { - const link = areaChart.vm.downloadLink; - - expect(link).toContain('blob:'); - }); - }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 36f650d5933..b78896c45fc 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -5,7 +5,7 @@ import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import * as types from '~/monitoring/stores/mutation_types'; import { createStore } from '~/monitoring/stores'; import axios from '~/lib/utils/axios_utils'; -import { +import MonitoringMock, { metricsGroupsAPIResponse, mockApiEndpoint, environmentData, @@ -40,6 +40,7 @@ describe('Dashboard', () => { let mock; let store; let component; + let mockGraphData; beforeEach(() => { setFixtures(` @@ -482,4 +483,36 @@ describe('Dashboard', () => { }); }); }); + + describe('when downloading metrics data as CSV', () => { + beforeEach(() => { + component = new DashboardComponent({ + propsData: { + ...propsData, + }, + store, + }); + store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + MonitoringMock.data, + ); + [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics; + }); + + describe('csvText', () => { + it('converts metrics data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadCsv', () => { + it('produces a link with a Blob', () => { + expect(component.downloadCsv(mockGraphData)).toContain(`blob:`); + }); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index ba3ba01944d..53e1f077610 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -236,24 +236,26 @@ describe('ReadyToMerge', () => { }); }); - describe('shouldShowMergeOptionsDropdown', () => { - it('should return false when no auto merge strategies are available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + describe('shouldShowMergeImmediatelyDropdown', () => { + it('should return false if no pipeline is active', () => { + Vue.set(vm.mr, 'isPipelineActive', false); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return true when at least one auto merge strategy is available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); + it('should return false if "Pipelines must succeed" is enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); - expect(vm.shouldShowMergeOptionsDropdown).toBe(true); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true); }); }); diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 099d7b6b67c..394efa85701 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -4,62 +4,131 @@ require 'spec_helper' describe ExpandVariables do describe '#expand' do - subject { described_class.expand(value, variables) } + context 'table tests' do + using RSpec::Parameterized::TableSyntax - tests = [ - { value: 'key', - result: 'key', - variables: [] }, - { value: 'key$variable', - result: 'key', - variables: [] }, - { value: 'key$variable', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] }, - { value: 'key${variable}', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] }, - { value: 'key$variable$variable2', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key${variable}${variable2}', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key$variable2$variable', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key${variable2}${variable}', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'review/$CI_COMMIT_REF_NAME', - result: 'review/feature/add-review-apps', - variables: [ - { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } - ] } - ] + where do + { + "no expansion": { + value: 'key', + result: 'key', + variables: [] + }, + "missing variable": { + value: 'key$variable', + result: 'key', + variables: [] + }, + "simple expansion": { + value: 'key$variable', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple with hash of variables": { + value: 'key$variable', + result: 'keyvalue', + variables: { + 'variable' => 'value' + } + }, + "complex expansion": { + value: 'key${variable}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple expansions": { + value: 'key$variable$variable2', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions": { + value: 'key${variable}${variable2}', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions with missing variable": { + value: 'key${variable}${variable2}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "out-of-order expansion": { + value: 'key$variable2$variable', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "out-of-order complex expansion": { + value: 'key${variable2}${variable}', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "review-apps expansion": { + value: 'review/$CI_COMMIT_REF_NAME', + result: 'review/feature/add-review-apps', + variables: [ + { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } + ] + }, + "do not lazily access variables when no expansion": { + value: 'key', + result: 'key', + variables: -> { raise NotImplementedError } + }, + "lazily access variables": { + value: 'key$variable', + result: 'keyvalue', + variables: -> { [{ key: 'variable', value: 'value' }] } + } + } + end + + with_them do + subject { ExpandVariables.expand(value, variables) } # rubocop:disable RSpec/DescribedClass + + it { is_expected.to eq(result) } + end + end + + context 'lazily inits variables' do + let(:variables) { -> { [{ key: 'variable', value: 'result' }] } } + + subject { described_class.expand(value, variables) } + + context 'when expanding variable' do + let(:value) { 'key$variable$variable2' } + + it 'calls block at most once' do + expect(variables).to receive(:call).once.and_call_original + + is_expected.to eq('keyresult') + end + end + + context 'when no expansion is needed' do + let(:value) { 'key' } - tests.each do |test| - context "#{test[:value]} resolves to #{test[:result]}" do - let(:value) { test[:value] } - let(:variables) { test[:variables] } + it 'does not call block' do + expect(variables).not_to receive(:call) - it { is_expected.to eq(test[:result]) } + is_expected.to eq('key') + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 800ef122203..415ade7a096 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -89,14 +89,23 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when has needs' do let(:config) do - { script: 'echo', needs: ['another-job'] } + { + stage: 'test', + script: 'echo', + needs: ['another-job'] + } end it { expect(entry).to be_valid } context 'when has dependencies' do let(:config) do - { script: 'echo', dependencies: ['another-job'], needs: ['another-job'] } + { + stage: 'test', + script: 'echo', + dependencies: ['another-job'], + needs: ['another-job'] + } end it { expect(entry).to be_valid } @@ -256,7 +265,11 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when has needs' do context 'that are not a array of strings' do let(:config) do - { script: 'echo', needs: 'build-job' } + { + stage: 'test', + script: 'echo', + needs: 'build-job' + } end it 'returns error about invalid type' do @@ -267,7 +280,12 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when have dependencies that are not subset of needs' do let(:config) do - { script: 'echo', dependencies: ['another-job'], needs: ['build-job'] } + { + stage: 'test', + script: 'echo', + dependencies: ['another-job'], + needs: ['build-job'] + } end it 'returns error about invalid data' do @@ -275,6 +293,20 @@ describe Gitlab::Ci::Config::Entry::Job do expect(entry.errors).to include 'job dependencies the another-job should be part of needs' end end + + context 'when stage: is missing' do + let(:config) do + { + script: 'echo', + needs: ['build-job'] + } + end + + it 'returns error about invalid data' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'job config missing required keys: stage' + end + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 762025f9bd9..5d4dec5899a 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -386,17 +386,28 @@ describe Gitlab::Ci::Pipeline::Seed::Build do describe 'applying needs: dependency' do subject { seed_build } + let(:needs_count) { 1 } + + let(:needs_attributes) do + Array.new(needs_count, name: 'build') + end + let(:attributes) do { name: 'rspec', - needs_attributes: [{ - name: 'build' - }] + needs_attributes: needs_attributes } end context 'when build job is not present in prior stages' do - it { is_expected.not_to be_included } + it "is included" do + is_expected.to be_included + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: needs 'build'") + end end context 'when build job is part of prior stages' do @@ -414,7 +425,39 @@ describe Gitlab::Ci::Pipeline::Seed::Build do let(:previous_stages) { [stage_seed] } - it { is_expected.to be_included } + it "is included" do + is_expected.to be_included + end + + it "does not have errors" do + expect(subject.errors).to be_empty + end + end + + context 'when lower limit of needs is reached' do + before do + stub_feature_flags(ci_dag_limit_needs: true) + end + + let(:needs_count) { described_class::LOW_NEEDS_LIMIT + 1 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 5 others, but you have listed 6. See needs keyword documentation for more details") + end + end + + context 'when upper limit of needs is reached' do + before do + stub_feature_flags(ci_dag_limit_needs: false) + end + + let(:needs_count) { described_class::HARD_NEEDS_LIMIT + 1 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 50 others, but you have listed 51. See needs keyword documentation for more details") + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 6fba9f37d91..a13335f63d5 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -121,6 +121,16 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do end end + describe '#seeds_errors' do + it 'returns all errors from seeds' do + expect(subject.seeds.first) + .to receive(:errors) { ["build error"] } + + expect(subject.errors).to contain_exactly( + "build error") + end + end + describe '#to_resource' do it 'builds a valid stage object with all builds' do subject.to_resource.save! diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb index 82614bb8238..6b548d5c4a8 100644 --- a/spec/lib/gitlab/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -25,7 +25,9 @@ describe Gitlab::Config::Entry::Attributable do end it 'returns the value of config' do + expect(instance).to have_name expect(instance.name).to eq 'some name' + expect(instance).to have_test expect(instance.test).to eq 'some test' end @@ -42,6 +44,7 @@ describe Gitlab::Config::Entry::Attributable do end it 'returns nil' do + expect(instance).not_to have_test expect(instance.test).to be_nil end end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cc31f88d365..e8a9f0b06a8 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -3,9 +3,43 @@ require 'spec_helper' describe Gitlab::DataBuilder::Push do + include RepoHelpers + let(:project) { create(:project, :repository) } let(:user) { build(:user, public_email: 'public-email@example.com') } + describe '.build' do + let(:sample) { RepoHelpers.sample_compare } + let(:commits) { project.repository.commits_between(sample.commits.first, sample.commits.last) } + let(:subject) do + described_class.build(project: project, + user: user, + ref: sample.target_branch, + commits: commits, + commits_count: commits.length, + message: 'test message', + with_changed_files: with_changed_files) + end + + context 'with changed files' do + let(:with_changed_files) { true } + + it 'returns commit hook data' do + expect(subject[:project]).to eq(project.hook_attrs) + expect(subject[:commits].first.keys).to include(*%i(added removed modified)) + end + end + + context 'without changed files' do + let(:with_changed_files) { false } + + it 'returns commit hook data without include deltas' do + expect(subject[:project]).to eq(project.hook_attrs) + expect(subject[:commits].first.keys).not_to include(*%i(added removed modified)) + end + end + end + describe '.build_sample' do let(:data) { described_class.build_sample(project, user) } diff --git a/spec/lib/gitlab/git_post_receive_spec.rb b/spec/lib/gitlab/git_post_receive_spec.rb new file mode 100644 index 00000000000..4c20d945585 --- /dev/null +++ b/spec/lib/gitlab/git_post_receive_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::GitPostReceive do + let(:project) { create(:project) } + + subject { described_class.new(project, "project-#{project.id}", changes.dup, {}) } + + describe '#includes_branches?' do + context 'with no branches' do + let(:changes) do + <<~EOF + 654321 210987 refs/nobranches/tag1 + 654322 210986 refs/tags/test1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns false' do + expect(subject.includes_branches?).to be_falsey + end + end + + context 'with branches' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/tag1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns true' do + expect(subject.includes_branches?).to be_truthy + end + end + + context 'with malformed changes' do + let(:changes) do + <<~EOF + ref/heads/1 a + somebranch refs/heads/2 + EOF + end + + it 'returns false' do + expect(subject.includes_branches?).to be_falsey + end + end + end + + describe '#includes_tags?' do + context 'with no tags' do + let(:changes) do + <<~EOF + 654321 210987 refs/notags/tag1 + 654322 210986 refs/heads/test1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns false' do + expect(subject.includes_tags?).to be_falsey + end + end + + context 'with tags' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/tag1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns true' do + expect(subject.includes_tags?).to be_truthy + end + end + + context 'with malformed changes' do + let(:changes) do + <<~EOF + ref/tags/1 a + sometag refs/tags/2 + EOF + end + + it 'returns false' do + expect(subject.includes_tags?).to be_falsey + end + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 06c8d127951..fce2aded786 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'generates the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') - expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.14.3-kube-1.11.10') expect(container.env.count).to eq(3) expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) expect(container.command).to match_array(["/bin/sh"]) diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb new file mode 100644 index 00000000000..f24ab5579df --- /dev/null +++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Kubernetes::KubectlCmd do + describe '.delete' do + it 'constructs string properly' do + args = %w(resource_type type --flag-1 --flag-2) + + expected_command = 'kubectl delete resource_type type --flag-1 --flag-2' + + expect(described_class.delete(*args)).to eq expected_command + end + end + + describe '.apply_file' do + context 'without optional args' do + it 'requires filename to be present' do + expect { described_class.apply_file(nil) }.to raise_error(ArgumentError, "filename is not present") + expect { described_class.apply_file(" ") }.to raise_error(ArgumentError, "filename is not present") + end + + it 'constructs string properly' do + expected_command = 'kubectl apply -f filename' + + expect(described_class.apply_file('filename')).to eq expected_command + end + end + + context 'with optional args' do + it 'constructs command properly with many args' do + args = %w(arg-1 --flag-0-1 arg-2 --flag-0-2) + + expected_command = 'kubectl apply -f filename arg-1 --flag-0-1 arg-2 --flag-0-2' + + expect(described_class.apply_file('filename', *args)).to eq expected_command + end + + it 'constructs command properly with single arg' do + args = "arg-1" + + expected_command = 'kubectl apply -f filename arg-1' + + expect(described_class.apply_file('filename', args)).to eq(expected_command) + end + end + end +end diff --git a/spec/lib/gitlab/snowplow_tracker_spec.rb b/spec/lib/gitlab/snowplow_tracker_spec.rb new file mode 100644 index 00000000000..073a33e5973 --- /dev/null +++ b/spec/lib/gitlab/snowplow_tracker_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::SnowplowTracker do + let(:timestamp) { Time.utc(2017, 3, 22) } + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + subject { described_class.track_event('epics', 'action', property: 'what', value: 'doit') } + + context '.track_event' do + context 'when Snowplow tracker is disabled' do + it 'does not track the event' do + expect(SnowplowTracker::Tracker).not_to receive(:new) + + subject + end + end + + context 'when Snowplow tracker is enabled' do + before do + stub_application_setting(snowplow_enabled: true) + stub_application_setting(snowplow_site_id: 'awesome gitlab') + stub_application_setting(snowplow_collector_hostname: 'url.com') + end + + it 'tracks the event' do + tracker = double + + expect(::SnowplowTracker::Tracker).to receive(:new) + .with( + an_instance_of(::SnowplowTracker::Emitter), + an_instance_of(::SnowplowTracker::Subject), + 'cf', 'awesome gitlab' + ).and_return(tracker) + expect(tracker).to receive(:track_struct_event) + .with('epics', 'action', nil, 'what', 'doit', nil, timestamp.to_i) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb new file mode 100644 index 00000000000..71be37692e2 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::CycleAnalyticsCounter do + it_behaves_like 'a redis usage counter', 'CycleAnalytics', :views + + it_behaves_like 'a redis usage counter with totals', :cycle_analytics, views: 3 +end diff --git a/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb new file mode 100644 index 00000000000..47077345e0c --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::SourceCodeCounter do + it_behaves_like 'a redis usage counter', 'Source Code', :pushes + + it_behaves_like 'a redis usage counter with totals', :source_code, pushes: 5 +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index bf36273251b..588c68d1fb0 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -59,6 +59,7 @@ describe Gitlab::UsageData do avg_cycle_analytics influxdb_metrics_enabled prometheus_metrics_enabled + cycle_analytics_views )) expect(subject).to include( @@ -71,7 +72,9 @@ describe Gitlab::UsageData do web_ide_views: a_kind_of(Integer), web_ide_commits: a_kind_of(Integer), web_ide_merge_requests: a_kind_of(Integer), - navbar_searches: a_kind_of(Integer) + navbar_searches: a_kind_of(Integer), + cycle_analytics_views: a_kind_of(Integer), + source_code_pushes: a_kind_of(Integer) ) end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index d4f8b552088..00b5c72a3d3 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -23,7 +23,7 @@ describe Clusters::Applications::Helm do Clusters::Cluster::APPLICATIONS.keys.each do |application_name| next if application_name == 'helm' - it do + it "is false when #{application_name} is installed" do cluster_application = create("clusters_applications_#{application_name}".to_sym) helm = cluster_application.cluster.application_helm @@ -31,6 +31,14 @@ describe Clusters::Applications::Helm do expect(helm.allowed_to_uninstall?).to be_falsy end end + + it 'executes a single query only' do + cluster_application = create(:clusters_applications_ingress) + helm = cluster_application.cluster.application_helm + + query_count = ActiveRecord::QueryRecorder.new { helm.allowed_to_uninstall? }.count + expect(query_count).to eq(1) + end end context "without other existing applications" do diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 619ab96af94..cf7c7bf7e61 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -42,9 +42,9 @@ describe ChatMessage::PipelineMessage do before do test_commit = double("A test commit", committer: args[:user], title: "A test commit message") - test_project = double("A test project", - commit_by: test_commit, name: args[:project][:name], - web_url: args[:project][:web_url], avatar_url: args[:project][:avatar_url]) + test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url]) + allow(test_project).to receive(:avatar_url).with(no_args).and_return("/avatar") + allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url]) allow(Project).to receive(:find) { test_project } test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors, diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 29a589eba20..2afe1253e29 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2252,6 +2252,21 @@ describe Project do end end + describe '#mark_stuck_remote_mirrors_as_failed!' do + it 'fails stuck remote mirrors' do + project = create(:project, :repository, :remote_mirror) + + project.remote_mirrors.first.update( + update_status: :started, + last_update_started_at: 2.days.ago + ) + + expect do + project.mark_stuck_remote_mirrors_as_failed! + end.to change { project.remote_mirrors.stuck.count }.from(1).to(0) + end + end + describe '#ancestors_upto' do let(:parent) { create(:group) } let(:child) { create(:group, parent: parent) } @@ -4297,6 +4312,39 @@ describe Project do end end + describe '#has_active_hooks?' do + set(:project) { create(:project) } + + it { expect(project.has_active_hooks?).to be_falsey } + + it 'returns true when a matching push hook exists' do + create(:project_hook, push_events: true, project: project) + + expect(project.has_active_hooks?(:merge_request_events)).to be_falsey + expect(project.has_active_hooks?).to be_truthy + end + + it 'returns true when a matching system hook exists' do + create(:system_hook, push_events: true) + + expect(project.has_active_hooks?(:merge_request_events)).to be_falsey + expect(project.has_active_hooks?).to be_truthy + end + end + + describe '#has_active_services?' do + set(:project) { create(:project) } + + it { expect(project.has_active_services?).to be_falsey } + + it 'returns true when a matching service exists' do + create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project) + + expect(project.has_active_services?(:merge_request_hooks)).to be_falsey + expect(project.has_active_services?).to be_truthy + end + end + describe '#badges' do let(:project_group) { create(:group) } let(:project) { create(:project, path: 'avatar', namespace: project_group) } diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 687b0935c55..7edeb56efe2 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -153,14 +153,14 @@ describe RemoteMirror, :mailer do end end - describe '#mark_as_failed' do + describe '#mark_as_failed!' do let(:remote_mirror) { create(:remote_mirror) } let(:error_message) { 'http://user:pass@test.com/root/repoC.git/' } let(:sanitized_error_message) { 'http://*****:*****@test.com/root/repoC.git/' } subject do remote_mirror.update_start - remote_mirror.mark_as_failed(error_message) + remote_mirror.mark_as_failed!(error_message) end it 'sets the update_status to failed' do @@ -204,8 +204,8 @@ describe RemoteMirror, :mailer do it 'includes mirrors that were started over an hour ago' do mirror = create_mirror(url: 'http://cantbeblank', update_status: 'started', - last_update_at: 3.hours.ago, - updated_at: 2.hours.ago) + last_update_started_at: 3.hours.ago, + last_update_at: 2.hours.ago) expect(described_class.stuck.last).to eq(mirror) end @@ -214,7 +214,7 @@ describe RemoteMirror, :mailer do mirror = create_mirror(url: 'http://cantbeblank', update_status: 'started', last_update_at: nil, - updated_at: 4.hours.ago) + last_update_started_at: 4.hours.ago) expect(described_class.stuck.last).to eq(mirror) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 12dff440ce2..e68de2e73a8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1744,12 +1744,23 @@ describe Repository do end end - describe '#before_push_tag' do + describe '#expires_caches_for_tags' do it 'flushes the cache' do expect(repository).to receive(:expire_statistics_caches) expect(repository).to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_tags_cache) + repository.expire_caches_for_tags + end + end + + describe '#before_push_tag' do + it 'logs an event' do + expect(repository).not_to receive(:expire_statistics_caches) + expect(repository).not_to receive(:expire_emptiness_caches) + expect(repository).not_to receive(:expire_tags_cache) + expect(repository).to receive(:repository_event).with(:push_tag) + repository.before_push_tag end end @@ -1781,6 +1792,12 @@ describe Repository do repository.after_create_branch end + + it 'does not expire the branch caches when specified' do + expect(repository).not_to receive(:expire_branches_cache) + + repository.after_create_branch(expire_cache: false) + end end describe '#after_remove_branch' do @@ -1789,6 +1806,12 @@ describe Repository do repository.after_remove_branch end + + it 'does not expire the branch caches when specified' do + expect(repository).not_to receive(:expire_branches_cache) + + repository.after_remove_branch(expire_cache: false) + end end describe '#after_create' do diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb index 7b61819e079..2373fef8aa6 100644 --- a/spec/policies/clusters/instance_policy_spec.rb +++ b/spec/policies/clusters/instance_policy_spec.rb @@ -9,6 +9,8 @@ describe Clusters::InstancePolicy do describe 'rules' do context 'when user' do it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :add_cluster } + it { expect(policy).to be_disallowed :create_cluster } it { expect(policy).to be_disallowed :update_cluster } it { expect(policy).to be_disallowed :admin_cluster } end @@ -17,6 +19,8 @@ describe Clusters::InstancePolicy do let(:user) { create(:admin) } it { expect(policy).to be_allowed :read_cluster } + it { expect(policy).to be_allowed :add_cluster } + it { expect(policy).to be_allowed :create_cluster } it { expect(policy).to be_allowed :update_cluster } it { expect(policy).to be_allowed :admin_cluster } end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index 12be3927e18..df6cc526eb0 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -226,4 +226,32 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:read_instance_statistics) } end end + + describe 'slash commands' do + context 'regular user' do + it { is_expected.to be_allowed(:use_slash_commands) } + end + + context 'when internal' do + let(:current_user) { User.ghost } + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + + context 'when blocked' do + before do + current_user.block + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + + context 'when access locked' do + before do + current_user.lock_access! + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 184c00a356a..590107d5161 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -144,6 +144,7 @@ describe API::Settings, 'Settings' do external_auth_client_key_pass: "5iveL!fe" } end + let(:attribute_names) { settings.keys.map(&:to_s) } it 'includes the attributes in the API' do @@ -165,6 +166,56 @@ describe API::Settings, 'Settings' do end end + context "snowplow tracking settings" do + let(:settings) do + { + snowplow_collector_hostname: "snowplow.example.com", + snowplow_cookie_domain: ".example.com", + snowplow_enabled: true, + snowplow_site_id: "site_id" + } + end + + let(:attribute_names) { settings.keys.map(&:to_s) } + + it "includes the attributes in the API" do + get api("/application/settings", admin) + + expect(response).to have_gitlab_http_status(200) + attribute_names.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + + it "allows updating the settings" do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(200) + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + + context "missing snowplow_collector_hostname value when snowplow_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), params: { snowplow_enabled: true } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to eq("snowplow_collector_hostname is missing") + end + + it "handles validation errors" do + put api("/application/settings", admin), params: settings.merge({ + snowplow_collector_hostname: nil + }) + + expect(response).to have_gitlab_http_status(400) + message = json_response["message"] + expect(message["snowplow_collector_hostname"]).to include("can't be blank") + end + end + end + context "missing plantuml_url value when plantuml_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { plantuml_enabled: true } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 7e2f311a065..deb68899309 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1113,7 +1113,7 @@ describe Ci::CreatePipelineService do test_a: { stage: "test", script: "ls", - only: %w[master feature tags], + only: %w[master feature], needs: %w[build_a] }, deploy: { @@ -1143,6 +1143,7 @@ describe Ci::CreatePipelineService do it 'does not create a pipeline as test_a depends on build_a' do expect(pipeline).not_to be_persisted expect(pipeline.builds).to be_empty + expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'") end end diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb index 4a2ec769116..874df9a68cd 100644 --- a/spec/services/git/base_hooks_service_spec.rb +++ b/spec/services/git/base_hooks_service_spec.rb @@ -14,6 +14,78 @@ describe Git::BaseHooksService do let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 let(:ref) { 'refs/tags/v1.1.0' } + describe '#execute_project_hooks' do + class TestService < described_class + def hook_name + :push_hooks + end + + def commits + [] + end + end + + let(:project) { create(:project, :repository) } + + subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } + + context '#execute_hooks' do + before do + expect(project).to receive(:has_active_hooks?).and_return(active) + end + + context 'active hooks' do + let(:active) { true } + + it 'executes the hooks' do + expect(subject).to receive(:push_data).at_least(:once).and_call_original + expect(project).to receive(:execute_hooks) + + subject.execute + end + end + + context 'inactive hooks' do + let(:active) { false } + + it 'does not execute the hooks' do + expect(subject).not_to receive(:push_data) + expect(project).not_to receive(:execute_hooks) + + subject.execute + end + end + end + + context '#execute_services' do + before do + expect(project).to receive(:has_active_services?).and_return(active) + end + + context 'active services' do + let(:active) { true } + + it 'executes the services' do + expect(subject).to receive(:push_data).at_least(:once).and_call_original + expect(project).to receive(:execute_services) + + subject.execute + end + end + + context 'inactive services' do + let(:active) { false } + + it 'does not execute the services' do + expect(subject).not_to receive(:push_data) + expect(project).not_to receive(:execute_services) + + subject.execute + end + end + end + end + describe 'with remote mirrors' do class TestService < described_class def commits diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 23be400059e..8af51848b7b 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -25,7 +25,7 @@ describe Git::BranchHooksService do end describe "Git Push Data" do - subject(:push_data) { service.execute } + subject(:push_data) { service.send(:push_data) } it 'has expected push data attributes' do is_expected.to match a_hash_including( @@ -109,6 +109,7 @@ describe Git::BranchHooksService do expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) expect(event.push_event_payload.commit_from).to eq(oldrev) expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.commit_title).to eq('Change some files') expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to eq(1) end @@ -124,6 +125,7 @@ describe Git::BranchHooksService do expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) expect(event.push_event_payload.commit_from).to be_nil expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.commit_title).to eq('Initial commit') expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to be > 1 end diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index 6e39fa6b3c0..ad5d296f5c1 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -78,7 +78,10 @@ describe Git::BranchPushService, services: true do it "creates a new pipeline" do expect { subject }.to change { Ci::Pipeline.count } - expect(Ci::Pipeline.last).to be_push + + pipeline = Ci::Pipeline.last + expect(pipeline).to be_push + expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref) end end @@ -123,6 +126,10 @@ describe Git::BranchPushService, services: true do describe "Webhooks" do context "execute webhooks" do + before do + create(:project_hook, push_events: true, project: project) + end + it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb index f5938a5c708..e362577d289 100644 --- a/spec/services/git/tag_hooks_service_spec.rb +++ b/spec/services/git/tag_hooks_service_spec.rb @@ -26,7 +26,8 @@ describe Git::TagHooksService, :service do describe 'System hooks' do it 'Executes system hooks' do - push_data = service.execute + push_data = service.send(:push_data) + expect(project).to receive(:has_active_hooks?).and_return(true) expect_next_instance_of(SystemHooksService) do |system_hooks_service| expect(system_hooks_service) @@ -40,6 +41,7 @@ describe Git::TagHooksService, :service do describe "Webhooks" do it "executes hooks on the project" do + expect(project).to receive(:has_active_hooks?).and_return(true) expect(project).to receive(:execute_hooks) service.execute @@ -61,7 +63,7 @@ describe Git::TagHooksService, :service do describe 'Push data' do shared_examples_for 'tag push data expectations' do - subject(:push_data) { service.execute } + subject(:push_data) { service.send(:push_data) } it 'has expected push data attributes' do is_expected.to match a_hash_including( object_kind: 'tag_push', diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index 418952b52da..7e008637182 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -26,8 +26,8 @@ describe Git::TagPushService do subject end - it 'flushes the tags cache' do - expect(project.repository).to receive(:expire_tags_cache) + it 'does not flush the tags cache' do + expect(project.repository).not_to receive(:expire_tags_cache) subject end diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb index be2811ab1e7..4396ccab584 100644 --- a/spec/services/projects/update_remote_mirror_service_spec.rb +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -10,49 +10,91 @@ describe Projects::UpdateRemoteMirrorService do subject(:service) { described_class.new(project, project.creator) } - describe "#execute" do + describe '#execute' do + subject(:execute!) { service.execute(remote_mirror, 0) } + before do project.repository.add_branch(project.owner, 'existing-branch', 'master') allow(remote_mirror).to receive(:update_repository).and_return(true) end - it "ensures the remote exists" do + it 'ensures the remote exists' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) expect(remote_mirror).to receive(:ensure_remote!) - service.execute(remote_mirror) + execute! end - it "fetches the remote repository" do + it 'fetches the remote repository' do expect(project.repository) .to receive(:fetch_remote) - .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror) + .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror) - service.execute(remote_mirror) + execute! end - it "returns success when updated succeeds" do + it 'marks the mirror as started when beginning' do + expect(remote_mirror).to receive(:update_start!).and_call_original + + execute! + end + + it 'marks the mirror as successfully finished' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) - result = service.execute(remote_mirror) + result = execute! expect(result[:status]).to eq(:success) + expect(remote_mirror).to be_finished + end + + it 'marks the mirror as failed and raises the error when an unexpected error occurs' do + allow(project.repository).to receive(:fetch_remote).and_raise('Badly broken') + + expect { execute! }.to raise_error /Badly broken/ + + expect(remote_mirror).to be_failed + expect(remote_mirror.last_error).to include('Badly broken') + end + + context 'when the update fails because of a `Gitlab::Git::CommandError`' do + before do + allow(project.repository).to receive(:fetch_remote).and_raise(Gitlab::Git::CommandError.new('fetch failed')) + end + + it 'wraps `Gitlab::Git::CommandError`s in a service error' do + expect(execute!).to eq(status: :error, message: 'fetch failed') + end + + it 'marks the mirror as to be retried' do + execute! + + expect(remote_mirror).to be_to_retry + expect(remote_mirror.last_error).to include('fetch failed') + end + + it "marks the mirror as failed after #{described_class::MAX_TRIES} tries" do + service.execute(remote_mirror, described_class::MAX_TRIES) + + expect(remote_mirror).to be_failed + expect(remote_mirror.last_error).to include('fetch failed') + end end context 'when syncing all branches' do - it "push all the branches the first time" do + it 'push all the branches the first time' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) expect(remote_mirror).to receive(:update_repository).with({}) - service.execute(remote_mirror) + execute! end end context 'when only syncing protected branches' do - it "sync updated protected branches" do + it 'sync updated protected branches' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) protected_branch = create_protected_branch(project) remote_mirror.only_protected_branches = true @@ -61,7 +103,7 @@ describe Projects::UpdateRemoteMirrorService do .to receive(:update_repository) .with(only_branches_matching: [protected_branch.name]) - service.execute(remote_mirror) + execute! end def create_protected_branch(project) diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb index 82975027e5b..dcc92dda950 100644 --- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb @@ -93,6 +93,19 @@ RSpec.shared_examples 'chat slash commands service' do subject.trigger(params) end + + context 'when user is blocked' do + before do + chat_name.user.block + end + + it 'blocks command execution' do + expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute) + + result = subject.trigger(params) + expect(result).to include(text: /^Whoops! This action is not allowed/) + end + end end end end diff --git a/spec/support/shared_examples/policies/clusterable_shared_examples.rb b/spec/support/shared_examples/policies/clusterable_shared_examples.rb index 4f9873d53e4..0b427c23256 100644 --- a/spec/support/shared_examples/policies/clusterable_shared_examples.rb +++ b/spec/support/shared_examples/policies/clusterable_shared_examples.rb @@ -13,7 +13,11 @@ shared_examples 'clusterable policies' do clusterable.add_developer(current_user) end + it { expect_disallowed(:read_cluster) } it { expect_disallowed(:add_cluster) } + it { expect_disallowed(:create_cluster) } + it { expect_disallowed(:update_cluster) } + it { expect_disallowed(:admin_cluster) } end context 'with a maintainer' do @@ -22,7 +26,11 @@ shared_examples 'clusterable policies' do end context 'with no clusters' do + it { expect_allowed(:read_cluster) } it { expect_allowed(:add_cluster) } + it { expect_allowed(:create_cluster) } + it { expect_allowed(:update_cluster) } + it { expect_allowed(:admin_cluster) } end end end diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index cbb4199954a..70cdc08b4b6 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -70,6 +70,23 @@ describe 'layouts/_head' do expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />') end + context 'when an asset_host is set and snowplow url is set' do + let(:asset_host) { 'http://test.host' } + + before do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + allow(Gitlab::CurrentSettings).to receive(:snowplow_enabled?).and_return(true) + allow(Gitlab::CurrentSettings).to receive(:snowplow_collector_hostname).and_return('www.snow.plow') + end + + it 'add a snowplow script tag with asset host' do + render + expect(rendered).to match('http://test.host/assets/snowplow/') + expect(rendered).to match('window.snowplow') + expect(rendered).to match('www.snow.plow') + end + end + def stub_helper_with_safe_string(method) allow_any_instance_of(PageLayoutHelper).to receive(method) .and_return(%q{foo" http-equiv="refresh}.html_safe) diff --git a/spec/workers/build_process_worker_spec.rb b/spec/workers/build_process_worker_spec.rb deleted file mode 100644 index d9a02ece142..00000000000 --- a/spec/workers/build_process_worker_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe BuildProcessWorker do - describe '#perform' do - context 'when build exists' do - let(:pipeline) { create(:ci_pipeline) } - let(:build) { create(:ci_build, pipeline: pipeline) } - - it 'processes build' do - expect_any_instance_of(Ci::Pipeline).to receive(:process!) - .with([build.id]) - - described_class.new.perform(build.id) - end - end - - context 'when build does not exist' do - it 'does not raise exception' do - expect { described_class.new.perform(123) } - .not_to raise_error - end - end - end -end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 39f1beb4efa..3b69b81f12e 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -14,6 +14,10 @@ describe PostReceive do create(:project, :repository, auto_cancel_pending_pipelines: 'disabled') end + def perform(changes: base64_changes) + described_class.new.perform(gl_repository, key_id, changes) + end + context "as a sidekiq worker" do it "responds to #perform" do expect(described_class.new).to respond_to(:perform) @@ -28,7 +32,7 @@ describe PostReceive do it "returns false and logs an error" do expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}") - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be(false) + expect(perform).to be(false) end end @@ -39,7 +43,7 @@ describe PostReceive do expect(Git::TagPushService).not_to receive(:new) expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } - described_class.new.perform(gl_repository, key_id, "") + perform(changes: "") end end @@ -50,40 +54,80 @@ describe PostReceive do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false + expect(perform).to be false end end context 'with changes' do before do allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) end context "branches" do - let(:changes) { "123456 789012 refs/heads/tést" } + let(:changes) do + <<~EOF + '123456 789012 refs/heads/tést1' + '123456 789012 refs/heads/tést2' + EOF + end - it "calls Git::BranchPushService" do - expect_next_instance_of(Git::BranchPushService) do |service| + it 'expires the branches cache' do + expect(project.repository).to receive(:expire_branches_cache).once + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + + it 'calls Git::BranchPushService' do + expect_any_instance_of(Git::BranchPushService) do |service| expect(service).to receive(:execute).and_return(true) end expect(Git::TagPushService).not_to receive(:new) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end context "tags" do - let(:changes) { "123456 789012 refs/tags/tag" } + let(:changes) do + <<~EOF + 654321 210987 refs/tags/tag1 + 654322 210986 refs/tags/tag2 + 654323 210985 refs/tags/tag3 + 654324 210984 refs/tags/tag4 + 654325 210983 refs/tags/tag5 + EOF + end + + before do + expect(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) + end + + it 'does not expire branches cache' do + expect(project.repository).not_to receive(:expire_branches_cache) + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + + it "only invalidates tags once" do + expect(project.repository).to receive(:repository_event).exactly(5).times.with(:push_tag).and_call_original + expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original + expect(project.repository).to receive(:expire_tags_cache).once.and_call_original + + described_class.new.perform(gl_repository, key_id, base64_changes) + end it "calls Git::TagPushService" do - expect(Git::BranchPushService).not_to receive(:execute) + expect(Git::BranchPushService).not_to receive(:new) - expect_next_instance_of(Git::TagPushService) do |service| + expect_any_instance_of(Git::TagPushService) do |service| expect(service).to receive(:execute).and_return(true) end - described_class.new.perform(gl_repository, key_id, base64_changes) + expect(Git::BranchPushService).not_to receive(:new) + + perform end end @@ -94,7 +138,7 @@ describe PostReceive do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end @@ -111,7 +155,7 @@ describe PostReceive do let(:changes_count) { changes.lines.count } - subject { described_class.new.perform(gl_repository, key_id, base64_changes) } + subject { perform } context "with valid .gitlab-ci.yml" do before do @@ -180,7 +224,13 @@ describe PostReceive do it 'calls SystemHooksService' do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform + end + + it 'increments the usage data counter of pushes event' do + counter = Gitlab::UsageDataCounters::SourceCodeCounter + + expect { perform }.to change { counter.read(:pushes) }.by(1) end end end @@ -197,7 +247,7 @@ describe PostReceive do # a second to ensure we see changes. Timecop.freeze(1.second.from_now) do expect do - described_class.new.perform(gl_repository, key_id, base64_changes) + perform project.reload end.to change(project, :last_activity_at) .and change(project, :last_repository_updated_at) @@ -208,7 +258,8 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_by).with(id: project.id.to_s) - described_class.new.perform(gl_repository, key_id, base64_changes) + + perform end it "does not run if the author is not in the project" do @@ -218,16 +269,18 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be_falsey + expect(perform).to be_falsey end it "asks the project to trigger all hooks" do + create(:project_hook, push_events: true, tag_push_events: true, project: project) + create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project) allow(Project).to receive(:find_by).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end it "enqueues a UpdateMergeRequestsWorker job" do @@ -235,7 +288,7 @@ describe PostReceive do expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end end diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb index 4de51ecb3e9..66d517332ba 100644 --- a/spec/workers/repository_update_remote_mirror_worker_spec.rb +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -2,99 +2,70 @@ require 'rails_helper' -describe RepositoryUpdateRemoteMirrorWorker do +describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do subject { described_class.new } - let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + let(:remote_mirror) { create(:remote_mirror) } let(:scheduled_time) { Time.now - 5.minutes } around do |example| Timecop.freeze(Time.now) { example.run } end - describe '#perform' do - context 'with status none' do - before do - remote_mirror.update(update_status: 'none') - end - - it 'sets status as finished when update remote mirror service executes successfully' do - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) - - expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished') - end - - it 'resets the notification flag upon success' do - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) - remote_mirror.update_column(:error_notification_sent, true) - - expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.error_notification_sent }.to(false) - end - - it 'sets status as failed when update remote mirror service executes with errors' do - error_message = 'fail!' - - expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| - expect(service).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message) - end + def expect_mirror_service_to_return(mirror, result, tries = 0) + expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| + expect(service).to receive(:execute).with(mirror, tries).and_return(result) + end + end - # Mock the finder so that it returns an object we can set expectations on - expect_next_instance_of(RemoteMirrorFinder) do |finder| - expect(finder).to receive(:execute).and_return(remote_mirror) - end - expect(remote_mirror).to receive(:mark_as_failed).with(error_message) + describe '#perform' do + it 'calls out to the service to perform the update' do + expect_mirror_service_to_return(remote_mirror, status: :success) - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message) - end + subject.perform(remote_mirror.id, scheduled_time) + end - it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do - remote_mirror.update(last_update_started_at: Time.now) + it 'does not do anything if the mirror was already updated' do + remote_mirror.update(last_update_started_at: Time.now, update_status: :finished) - expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true) - expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror) + expect(Projects::UpdateRemoteMirrorService).not_to receive(:new) - expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil - end + subject.perform(remote_mirror.id, scheduled_time) end - context 'with unexpected error' do - it 'marks mirror as failed' do - allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError) + it 'schedules a retry when the mirror is marked for retrying' do + remote_mirror = create(:remote_mirror, update_status: :to_retry) + expect_mirror_service_to_return(remote_mirror, status: :error, message: 'Retry!') - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError) - expect(remote_mirror.reload.update_status).to eq('failed') - end - end + expect(described_class) + .to receive(:perform_in) + .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 1) - context 'with another worker already running' do - before do - remote_mirror.update(update_status: 'started') - end - - it 'raises RemoteMirrorUpdateAlreadyInProgressError' do - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError) - end + subject.perform(remote_mirror.id, scheduled_time) end - context 'with status failed' do - before do - remote_mirror.update(update_status: 'failed') + it 'clears the lease if there was an unexpected exception' do + expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| + expect(service).to receive(:execute).with(remote_mirror, 1).and_raise('Unexpected!') end + expect { subject.perform(remote_mirror.id, Time.now, 1) }.to raise_error('Unexpected!') - it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do - remote_mirror.update(last_update_started_at: Time.now) + lease = Gitlab::ExclusiveLease.new("#{described_class.name}:#{remote_mirror.id}", timeout: 1.second) - expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false) - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + expect(lease.try_obtain).not_to be_nil + end - expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished') - end + it 'retries 3 times for the worker to finish before rescheduling' do + expect(subject).to receive(:in_lock) + .with("#{described_class.name}:#{remote_mirror.id}", + retries: 3, + ttl: remote_mirror.max_runtime, + sleep_sec: described_class::LOCK_WAIT_TIME) + .and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + expect(described_class).to receive(:perform_in) + .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 0) + + subject.perform(remote_mirror.id, scheduled_time) end end end |