summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue51
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue16
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue15
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue33
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js22
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js4
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js15
-rw-r--r--app/assets/javascripts/monitoring/utils.js5
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/controllers/admin/services_controller.rb12
-rw-r--r--app/controllers/concerns/lfs_request.rb4
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb11
-rw-r--r--app/models/concerns/sortable.rb14
-rw-r--r--app/models/project.rb55
-rw-r--r--app/models/project_services/issue_tracker_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb2
-rw-r--r--app/models/service.rb20
-rw-r--r--app/presenters/project_presenter.rb2
-rw-r--r--app/services/merge_requests/create_service.rb5
-rw-r--r--app/services/merge_requests/link_lfs_objects_service.rb31
-rw-r--r--app/services/merge_requests/refresh_service.rb29
-rw-r--r--app/services/projects/create_service.rb8
-rw-r--r--app/services/projects/fork_service.rb12
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb6
-rw-r--r--app/services/projects/move_lfs_objects_projects_service.rb2
-rw-r--r--app/services/projects/propagate_instance_level_service.rb (renamed from app/services/projects/propagate_service_template.rb)28
-rw-r--r--app/services/projects/unlink_fork_service.rb4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml4
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/propagate_instance_level_service_worker.rb26
-rw-r--r--app/workers/propagate_service_template_worker.rb26
-rw-r--r--app/workers/repository_fork_worker.rb26
34 files changed, 340 insertions, 158 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index e58d9f23201..8b2c5e44bb5 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,5 +1,5 @@
<script>
-import { omit } from 'lodash';
+import { omit, throttle } from 'lodash';
import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
@@ -18,6 +18,13 @@ import {
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
+const THROTTLED_DATAZOOM_WAIT = 1000; // miliseconds
+const timestampToISODate = timestamp => new Date(timestamp).toISOString();
+
+const events = {
+ datazoom: 'datazoom',
+};
+
export default {
components: {
GlAreaChart,
@@ -98,6 +105,7 @@ export default {
height: chartHeight,
svgs: {},
primaryColor: null,
+ throttledDatazoom: null,
};
},
computed: {
@@ -245,6 +253,11 @@ export default {
this.setSvg('rocket');
this.setSvg('scroll-handle');
},
+ destroyed() {
+ if (this.throttledDatazoom) {
+ this.throttledDatazoom.cancel();
+ }
+ },
methods: {
formatLegendLabel(query) {
return `${query.label}`;
@@ -287,8 +300,39 @@ export default {
console.error('SVG could not be rendered correctly: ', e);
});
},
- onChartUpdated(chart) {
- [this.primaryColor] = chart.getOption().color;
+ onChartUpdated(eChart) {
+ [this.primaryColor] = eChart.getOption().color;
+ },
+
+ onChartCreated(eChart) {
+ // Emit a datazoom event that corresponds to the eChart
+ // `datazoom` event.
+
+ if (this.throttledDatazoom) {
+ // Chart can be created multiple times in this component's
+ // lifetime, remove previous handlers every time
+ // chart is created.
+ this.throttledDatazoom.cancel();
+ }
+
+ // Emitting is throttled to avoid flurries of calls when
+ // the user changes or scrolls the zoom bar.
+ this.throttledDatazoom = throttle(
+ () => {
+ const { startValue, endValue } = eChart.getOption().dataZoom[0];
+ this.$emit(events.datazoom, {
+ start: timestampToISODate(startValue),
+ end: timestampToISODate(endValue),
+ });
+ },
+ THROTTLED_DATAZOOM_WAIT,
+ {
+ leading: false,
+ },
+ );
+
+ eChart.off('datazoom');
+ eChart.on('datazoom', this.throttledDatazoom);
},
onResize() {
if (!this.$refs.chart) return;
@@ -331,6 +375,7 @@ export default {
:height="height"
:average-text="legendAverageText"
:max-text="legendMaxText"
+ @created="onChartCreated"
@updated="onChartUpdated"
>
<template v-if="tooltip.isDeployment">
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 391cd6dd15e..79f32b357fc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -21,7 +21,6 @@ import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import { convertToFixedRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
@@ -102,6 +101,11 @@ export default {
type: String,
required: true,
},
+ logsPath: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
defaultBranch: {
type: String,
required: true,
@@ -247,22 +251,20 @@ export default {
dashboardsEndpoint: this.dashboardsEndpoint,
currentDashboard: this.currentDashboard,
projectPath: this.projectPath,
+ logsPath: this.logsPath,
});
},
mounted() {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
} else {
- const { start, end } = convertToFixedRange(this.selectedTimeRange);
-
- this.fetchData({
- start,
- end,
- });
+ this.setTimeRange(this.selectedTimeRange);
+ this.fetchData();
}
},
methods: {
...mapActions('monitoringDashboard', [
+ 'setTimeRange',
'fetchData',
'setGettingStartedEmptyState',
'setEndpoints',
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index e55de1c0105..49188a7af8f 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -19,14 +19,8 @@ export default {
},
data() {
const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange;
- const { start, end } = convertToFixedRange(timeRange);
- const params = {
- start,
- end,
- };
-
return {
- params,
+ timeRange: convertToFixedRange(timeRange),
elWidth: 0,
};
},
@@ -49,7 +43,9 @@ export default {
},
mounted() {
this.setInitialState();
- this.fetchMetricsData(this.params);
+ this.setTimeRange(this.timeRange);
+ this.fetchDashboard();
+
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true,
@@ -64,7 +60,8 @@ export default {
},
methods: {
...mapActions('monitoringDashboard', [
- 'fetchMetricsData',
+ 'setTimeRange',
+ 'fetchDashboard',
'setEndpoints',
'setFeatureFlags',
'setShowErrorBanner',
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 6751f3d31e8..0258a62b390 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -1,6 +1,7 @@
<script>
import { mapState } from 'vuex';
import { pickBy } from 'lodash';
+import invalidUrl from '~/lib/utils/invalid_url';
import {
GlDropdown,
GlDropdownItem,
@@ -18,7 +19,7 @@ import MonitorColumnChart from './charts/column.vue';
import MonitorStackedColumnChart from './charts/stacked_column.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default {
components: {
@@ -58,8 +59,13 @@ export default {
default: 'panel-type-chart',
},
},
+ data() {
+ return {
+ zoomedTimeRange: null,
+ };
+ },
computed: {
- ...mapState('monitoringDashboard', ['deploymentData', 'projectPath']),
+ ...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']),
alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
@@ -70,6 +76,14 @@ export default {
this.graphData.metrics[0].result.length > 0
);
},
+ logsPathWithTimeRange() {
+ const timeRange = this.zoomedTimeRange || this.timeRange;
+
+ if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
+ return timeRangeToUrl(timeRange, this.logsPath);
+ }
+ return null;
+ },
csvText() {
const chartData = this.graphData.metrics[0].result[0].values;
const yLabel = this.graphData.y_label;
@@ -107,6 +121,10 @@ export default {
},
downloadCSVOptions,
generateLinkToChartOptions,
+
+ onDatazoom({ start, end }) {
+ this.zoomedTimeRange = { start, end };
+ },
},
};
</script>
@@ -130,11 +148,13 @@ export default {
<component
:is="monitorChartComponent"
v-else-if="graphDataHasMetrics"
+ ref="timeChart"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
+ @datazoom="onDatazoom"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -157,6 +177,15 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
+
+ <gl-dropdown-item
+ v-if="logsPathWithTimeRange"
+ ref="viewLogsLink"
+ :href="logsPathWithTimeRange"
+ >
+ {{ s__('Metrics|View logs') }}
+ </gl-dropdown-item>
+
<gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv"
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 29000475bd4..3a052200ab9 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,6 +1,7 @@
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
@@ -32,6 +33,10 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
+export const setTimeRange = ({ commit }, timeRange) => {
+ commit(types.SET_TIME_RANGE, timeRange);
+};
+
export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData');
@@ -63,19 +68,24 @@ export const receiveEnvironmentsDataSuccess = ({ commit }, data) =>
export const receiveEnvironmentsDataFailure = ({ commit }) =>
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
-export const fetchData = ({ dispatch }, params) => {
- dispatch('fetchMetricsData', params);
+export const fetchData = ({ dispatch }) => {
+ dispatch('fetchDashboard');
dispatch('fetchDeploymentsData');
dispatch('fetchEnvironmentsData');
};
-export const fetchMetricsData = ({ dispatch }, params) => dispatch('fetchDashboard', params);
-
-export const fetchDashboard = ({ state, dispatch }, params) => {
+export const fetchDashboard = ({ state, dispatch }) => {
dispatch('requestMetricsDashboard');
+ const params = {};
+
+ if (state.timeRange) {
+ const { start, end } = convertToFixedRange(state.timeRange);
+ params.start = start;
+ params.end = end;
+ }
+
if (state.currentDashboard) {
- // eslint-disable-next-line no-param-reassign
params.dashboard = state.currentDashboard;
}
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index bdfaf42b35c..8873142accc 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -14,7 +14,7 @@ export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
-export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
+export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 2a86a6a26d8..5f559290ff7 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -182,6 +182,10 @@ export default {
state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
+ state.logsPath = endpoints.logsPath || state.logsPath;
+ },
+ [types.SET_TIME_RANGE](state, timeRange) {
+ state.timeRange = timeRange;
},
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 9d3227e8aae..a2050f8e893 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,22 +1,31 @@
import invalidUrl from '~/lib/utils/invalid_url';
export default () => ({
+ // API endpoints
metricsEndpoint: null,
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
+
+ // Dashboard request parameters
+ timeRange: null,
+ currentDashboard: null,
+
+ // Dashboard data
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
-
dashboard: {
panel_groups: [],
},
+ allDashboards: [],
+ // Other project data
deploymentData: [],
environments: [],
environmentsSearchTerm: '',
environmentsLoading: false,
- allDashboards: [],
- currentDashboard: null,
+
+ // GitLab paths to other pages
projectPath: null,
+ logsPath: invalidUrl,
});
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 915812596c6..b2fa44835e6 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -103,8 +103,9 @@ export const graphDataValidatorForAnomalyValues = graphData => {
/**
* Returns a time range from the current URL params
*
- * @returns {Object} The time range defined by the
- * current URL, reading from `window.location.search`
+ * @returns {Object|null} The time range defined by the
+ * current URL, reading from search query or `window.location.search`.
+ * Returns `null` if no parameters form a time range.
*/
export const timeRangeFromUrl = (search = window.location.search) => {
const params = queryToObject(search);
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index a1bfa03a5ac..6cd69fe75ce 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -102,6 +102,7 @@
padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-text-color;
+ overflow: hidden;
&:first-child {
margin-top: 0;
@@ -115,6 +116,7 @@
padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-text-color;
+ overflow: hidden;
}
h3 {
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index e31e0e09978..9cea25cc7fe 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -7,7 +7,7 @@ class Admin::ServicesController < Admin::ApplicationController
before_action :service, only: [:edit, :update]
def index
- @services = services_templates
+ @services = instance_level_services
end
def edit
@@ -19,7 +19,7 @@ class Admin::ServicesController < Admin::ApplicationController
def update
if service.update(service_params[:service])
- PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
+ PropagateInstanceLevelServiceWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
@@ -31,17 +31,17 @@ class Admin::ServicesController < Admin::ApplicationController
private
# rubocop: disable CodeReuse/ActiveRecord
- def services_templates
+ def instance_level_services
Service.available_services_names.map do |service_name|
- service_template = "#{service_name}_service".camelize.constantize
- service_template.where(template: true).first_or_create
+ service = "#{service_name}_service".camelize.constantize
+ service.where(instance: true).first_or_create
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def service
- @service ||= Service.where(id: params[:id], template: true).first
+ @service ||= Service.where(id: params[:id], instance: true).first
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 61072eec535..3152d959ae4 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -112,10 +112,6 @@ module LfsRequest
has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end
- def storage_project
- @storage_project ||= project.lfs_storage_project
- end
-
def objects
@objects ||= (params[:objects] || []).to_a
end
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index 58f496e16d3..ec5ca5bbeec 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -80,12 +80,13 @@ module Repositories
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
- # rubocop: disable CodeReuse/ActiveRecord
def link_to_project!(object)
- if object && !object.projects.exists?(storage_project.id)
- object.lfs_objects_projects.create!(project: storage_project)
- end
+ return unless object
+
+ LfsObjectsProject.safe_find_or_create_by!(
+ project: project,
+ lfs_object: object
+ )
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index c4af1b1fab2..4fe2a0e1827 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -8,13 +8,13 @@ module Sortable
extend ActiveSupport::Concern
included do
- scope :with_order_id_desc, -> { order(id: :desc) }
- scope :order_id_desc, -> { reorder(id: :desc) }
- scope :order_id_asc, -> { reorder(id: :asc) }
- scope :order_created_desc, -> { reorder(created_at: :desc) }
- scope :order_created_asc, -> { reorder(created_at: :asc) }
- scope :order_updated_desc, -> { reorder(updated_at: :desc) }
- scope :order_updated_asc, -> { reorder(updated_at: :asc) }
+ scope :with_order_id_desc, -> { order(self.arel_table['id'].desc) }
+ scope :order_id_desc, -> { reorder(self.arel_table['id'].desc) }
+ scope :order_id_asc, -> { reorder(self.arel_table['id'].asc) }
+ scope :order_created_desc, -> { reorder(self.arel_table['created_at'].desc) }
+ scope :order_created_asc, -> { reorder(self.arel_table['created_at'].asc) }
+ scope :order_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) }
+ scope :order_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) }
scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 816d964519d..717075161aa 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -358,6 +358,8 @@ class Project < ApplicationRecord
project_path: true,
length: { maximum: 255 }
+ validates :project_feature, presence: true
+
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
@@ -395,11 +397,11 @@ class Project < ApplicationRecord
# last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
scope :sorted_by_activity, -> { reorder(Arel.sql("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC")) }
- scope :sorted_by_stars_desc, -> { reorder(star_count: :desc) }
- scope :sorted_by_stars_asc, -> { reorder(star_count: :asc) }
+ scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
+ scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) }
scope :sorted_by_name_asc_limited, ->(limit) { reorder(name: :asc).limit(limit) }
# Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
- scope :projects_order_id_desc, -> { reorder("#{table_name}.id DESC") }
+ scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -595,9 +597,9 @@ class Project < ApplicationRecord
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc'
- reorder(last_activity_at: :desc)
+ reorder(self.arel_table['last_activity_at'].desc)
when 'latest_activity_asc'
- reorder(last_activity_at: :asc)
+ reorder(self.arel_table['last_activity_at'].asc)
when 'stars_desc'
sorted_by_stars_desc
when 'stars_asc'
@@ -1219,13 +1221,13 @@ class Project < ApplicationRecord
service = find_service(services, name)
return service if service
- # We should check if template for the service exists
- template = find_service(services_templates, name)
+ # We should check if an instance-level service exists
+ instance_level_service = find_service(instance_level_services, name)
- if template
- Service.build_from_template(id, template)
+ if instance_level_service
+ Service.build_from_instance(id, instance_level_service)
else
- # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ # If no instance-level service exists, we should create a new service. Ex `build_gitlab_ci_service`
public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -1357,6 +1359,10 @@ class Project < ApplicationRecord
forked_from_project || fork_network&.root_project
end
+ # TODO: Remove this method once all LfsObjectsProject records are backfilled
+ # for forks.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
def lfs_storage_project
@lfs_storage_project ||= begin
result = self
@@ -1369,14 +1375,27 @@ class Project < ApplicationRecord
end
end
- # This will return all `lfs_objects` that are accessible to the project.
- # So this might be `self.lfs_objects` if the project is not part of a fork
- # network, or it is the base of the fork network.
+ # This will return all `lfs_objects` that are accessible to the project and
+ # the fork source. This is needed since older forks won't have access to some
+ # LFS objects directly and have to get it from the fork source.
+ #
+ # TODO: Remove this method once all LfsObjectsProject records are backfilled
+ # for forks. At that point, projects can look at their own `lfs_objects`.
#
- # TODO: refactor this to get the correct lfs objects when implementing
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/39769
+ # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
def all_lfs_objects
- lfs_storage_project.lfs_objects
+ LfsObject
+ .distinct
+ .joins(:lfs_objects_projects)
+ .where(lfs_objects_projects: { project_id: [self, lfs_storage_project] })
+ end
+
+ # TODO: Call `#lfs_objects` instead once all LfsObjectsProject records are
+ # backfilled. At that point, projects can look at their own `lfs_objects`.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
+ def lfs_objects_oids
+ all_lfs_objects.pluck(:oid)
end
def personal?
@@ -2438,8 +2457,8 @@ class Project < ApplicationRecord
end
end
- def services_templates
- @services_templates ||= Service.where(template: true)
+ def instance_level_services
+ @instance_level_services ||= Service.where(instance: true)
end
def ensure_pages_metadatum
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 9e1393196ff..2ccf8e094e6 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -164,7 +164,7 @@ class IssueTrackerService < Service
end
def one_issue_tracker
- return if template?
+ return if instance?
return if project.blank?
if project.services.external_issue_trackers.where.not(id: id).any?
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 00b06ae2595..72dabe2578a 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -85,7 +85,7 @@ class PrometheusService < MonitoringService
end
def prometheus_available?
- return false if template?
+ return false if instance?
return false unless project
project.all_clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
diff --git a/app/models/service.rb b/app/models/service.rb
index 95b7c6927cf..6d0e375e757 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -32,7 +32,7 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services
has_one :service_hook
- validates :project_id, presence: true, unless: proc { |service| service.template? }
+ validates :project_id, presence: true, unless: proc { |service| service.instance? }
validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
@@ -70,8 +70,8 @@ class Service < ApplicationRecord
true
end
- def template?
- template
+ def instance?
+ instance
end
def category
@@ -299,15 +299,15 @@ class Service < ApplicationRecord
service_names.sort_by(&:downcase)
end
- def self.build_from_template(project_id, template)
- service = template.dup
+ def self.build_from_instance(project_id, instance_level_service)
+ service = instance_level_service.dup
- if template.supports_data_fields?
- data_fields = template.data_fields.dup
+ if instance_level_service.supports_data_fields?
+ data_fields = instance_level_service.data_fields.dup
data_fields.service = service
end
- service.template = false
+ service.instance = false
service.project_id = project_id
service.active = false if service.active? && !service.valid?
service
@@ -321,10 +321,6 @@ class Service < ApplicationRecord
nil
end
- def self.find_by_template
- find_by(template: true)
- end
-
# override if needed
def supports_data_fields?
false
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 42c707908e6..3af6be26843 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -302,7 +302,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project)
AnchorData.new(false,
- _('Kubernetes configured'),
+ _('Kubernetes'),
cluster_link,
'default')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 9a37a0330fc..4a05d1fd7ef 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -27,6 +27,7 @@ module MergeRequests
create_pipeline_for(issuable, current_user)
issuable.update_head_pipeline
Gitlab::UsageDataCounters::MergeRequestCounter.count(:create)
+ link_lfs_objects(issuable)
super
end
@@ -64,6 +65,10 @@ module MergeRequests
raise Gitlab::Access::AccessDeniedError
end
end
+
+ def link_lfs_objects(issuable)
+ LinkLfsObjectsService.new(issuable.target_project).execute(issuable)
+ end
end
end
diff --git a/app/services/merge_requests/link_lfs_objects_service.rb b/app/services/merge_requests/link_lfs_objects_service.rb
new file mode 100644
index 00000000000..191da594095
--- /dev/null
+++ b/app/services/merge_requests/link_lfs_objects_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class LinkLfsObjectsService < ::BaseService
+ def execute(merge_request, oldrev: merge_request.diff_base_sha, newrev: merge_request.diff_head_sha)
+ return if merge_request.source_project == project
+ return if no_changes?(oldrev, newrev)
+
+ new_lfs_oids = lfs_oids(merge_request.source_project.repository, oldrev, newrev)
+
+ return if new_lfs_oids.empty?
+
+ Projects::LfsPointers::LfsLinkService
+ .new(project)
+ .execute(new_lfs_oids)
+ end
+
+ private
+
+ def no_changes?(oldrev, newrev)
+ oldrev == newrev
+ end
+
+ def lfs_oids(source_repository, oldrev, newrev)
+ Gitlab::Git::LfsChanges
+ .new(source_repository, newrev)
+ .new_pointers(not_in: [oldrev])
+ .map(&:lfs_oid)
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 396ddec6383..c6e1651fa26 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
# empty diff during a manual merge
close_upon_missing_source_branch_ref
post_merge_manually_merged
+ link_forks_lfs_objects
reload_merge_requests
outdate_suggestions
refresh_pipelines_on_merge_requests
@@ -91,17 +92,25 @@ module MergeRequests
end
# rubocop: enable CodeReuse/ActiveRecord
+ # Link LFS objects that exists in forks but does not exists in merge requests
+ # target project
+ def link_forks_lfs_objects
+ return unless @push.branch_updated?
+
+ merge_requests_for_forks.find_each do |mr|
+ LinkLfsObjectsService
+ .new(mr.target_project)
+ .execute(mr, oldrev: @push.oldrev, newrev: @push.newrev)
+ end
+ end
+
# Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too
- # rubocop: disable CodeReuse/ActiveRecord
def reload_merge_requests
merge_requests = @project.merge_requests.opened
.by_source_or_target_branch(@push.branch_name).to_a
- # Fork merge requests
- merge_requests += MergeRequest.opened
- .where(source_branch: @push.branch_name, source_project: @project)
- .where.not(target_project: @project).to_a
+ merge_requests += merge_requests_for_forks.to_a
filter_merge_requests(merge_requests).each do |merge_request|
if branch_and_project_match?(merge_request) || @push.force_push?
@@ -117,7 +126,6 @@ module MergeRequests
# @source_merge_requests diffs (for MergeRequest#commit_shas for instance).
merge_requests_for_source_branch(reload: true)
end
- # rubocop: enable CodeReuse/ActiveRecord
def push_commit_ids
@push_commit_ids ||= @commits.map(&:id)
@@ -282,6 +290,15 @@ module MergeRequests
@source_merge_requests = nil if reload
@source_merge_requests ||= merge_requests_for(@push.branch_name)
end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def merge_requests_for_forks
+ @merge_requests_for_forks ||=
+ MergeRequest.opened
+ .where(source_branch: @push.branch_name, source_project: @project)
+ .where.not(target_project: @project)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index ef06545b27d..f3666f100a3 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -134,7 +134,7 @@ module Projects
if @project.save
unless @project.gitlab_project_import?
- create_services_from_active_templates(@project)
+ create_services_from_active_instance_level_services(@project)
@project.create_labels
end
@@ -160,9 +160,9 @@ module Projects
end
# rubocop: disable CodeReuse/ActiveRecord
- def create_services_from_active_templates(project)
- Service.where(template: true, active: true).each do |template|
- service = Service.build_from_template(project.id, template)
+ def create_services_from_active_instance_level_services(project)
+ Service.where(instance: true, active: true).each do |template|
+ service = Service.build_from_instance(project.id, template)
service.save!
end
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index e66a0ed181a..fcfea567885 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -26,17 +26,7 @@ module Projects
build_fork_network_member(fork_to_project)
- if link_fork_network(fork_to_project)
- # A forked project stores its LFS objects in the `forked_from_project`.
- # So the LFS objects become inaccessible, and therefore delete them from
- # the database so they'll get cleaned up.
- #
- # TODO: refactor this to get the correct lfs objects when implementing
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/39769
- fork_to_project.lfs_objects_projects.delete_all
-
- fork_to_project
- end
+ fork_to_project if link_fork_network(fork_to_project)
end
def fork_new_project
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index a009f479d5d..bd70012c76c 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -39,9 +39,9 @@ module Projects
def download_lfs_file!
with_tmp_file do |tmp_file|
download_and_save_file!(tmp_file)
- project.all_lfs_objects << LfsObject.new(oid: lfs_oid,
- size: lfs_size,
- file: tmp_file)
+ project.lfs_objects << LfsObject.new(oid: lfs_oid,
+ size: lfs_size,
+ file: tmp_file)
success
end
diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb
index 10e19014db4..8cc420d7ba7 100644
--- a/app/services/projects/move_lfs_objects_projects_service.rb
+++ b/app/services/projects/move_lfs_objects_projects_service.rb
@@ -16,7 +16,7 @@ module Projects
private
def move_lfs_objects_projects
- non_existent_lfs_objects_projects.update_all(project_id: @project.lfs_storage_project.id)
+ non_existent_lfs_objects_projects.update_all(project_id: @project.id)
end
def remove_remaining_lfs_objects_project
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_instance_level_service.rb
index 6013b00b8c6..dc75977ba0f 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_instance_level_service.rb
@@ -1,38 +1,38 @@
# frozen_string_literal: true
module Projects
- class PropagateServiceTemplate
+ class PropagateInstanceLevelService
BATCH_SIZE = 100
def self.propagate(*args)
new(*args).propagate
end
- def initialize(template)
- @template = template
+ def initialize(instance_level_service)
+ @instance_level_service = instance_level_service
end
def propagate
- return unless @template.active?
+ return unless @instance_level_service.active?
- Rails.logger.info("Propagating services for template #{@template.id}") # rubocop:disable Gitlab/RailsLogger
+ Rails.logger.info("Propagating services for instance_level_service #{@instance_level_service.id}") # rubocop:disable Gitlab/RailsLogger
- propagate_projects_with_template
+ propagate_projects_with_instance_level_service
end
private
- def propagate_projects_with_template
+ def propagate_projects_with_instance_level_service
loop do
batch = Project.uncached { project_ids_batch }
- bulk_create_from_template(batch) unless batch.empty?
+ bulk_create_from_instance_level_service(batch) unless batch.empty?
break if batch.size < BATCH_SIZE
end
end
- def bulk_create_from_template(batch)
+ def bulk_create_from_instance_level_service(batch)
service_list = batch.map do |project_id|
service_hash.values << project_id
end
@@ -52,7 +52,7 @@ module Projects
SELECT true
FROM services
WHERE services.project_id = projects.id
- AND services.type = '#{@template.type}'
+ AND services.type = '#{@instance_level_service.type}'
)
AND projects.pending_delete = false
AND projects.archived = false
@@ -73,9 +73,9 @@ module Projects
def service_hash
@service_hash ||=
begin
- template_hash = @template.as_json(methods: :type).except('id', 'template', 'project_id')
+ instance_hash = @instance_level_service.as_json(methods: :type).except('id', 'instance', 'project_id')
- template_hash.each_with_object({}) do |(key, value), service_hash|
+ instance_hash.each_with_object({}) do |(key, value), service_hash|
value = value.is_a?(Hash) ? value.to_json : value
service_hash[ActiveRecord::Base.connection.quote_column_name(key)] =
@@ -97,11 +97,11 @@ module Projects
# rubocop: enable CodeReuse/ActiveRecord
def active_external_issue_tracker?
- @template.issue_tracker? && !@template.default
+ @instance_level_service.issue_tracker? && !@instance_level_service.default
end
def active_external_wiki?
- @template.type == 'ExternalWikiService'
+ @instance_level_service.type == 'ExternalWikiService'
end
end
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index e7e0141099e..b3cf27373cd 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -52,6 +52,10 @@ module Projects
Projects::ForksCountService.new(project).refresh_cache
end
+ # TODO: Remove this method once all LfsObjectsProject records are backfilled
+ # for forks.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
def save_lfs_objects
return unless @project.forked?
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index cc005dd69b7..7f14128a0cb 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -10,8 +10,8 @@
%p.inline
= s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering")
%kbd.inline /&lt;trigger&gt; help
- - unless enabled || @service.template?
+ - unless enabled || @service.instance?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
-- if enabled && !@service.template?
+- if enabled && !@service.instance?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 7f6717e298c..447f7f074a8 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -11,7 +11,7 @@
%p.inline
= s_("SlackService|See list of available commands in Slack after setting up this service, by entering")
%kbd.inline /&lt;command&gt; help
- - unless @service.template?
+ - unless @service.instance?
%p= _("To set up this service:")
%ul.list-unstyled.indent-list
%li
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 35852742b0d..b344e1e36b8 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -951,7 +951,7 @@
:latency_sensitive:
:resource_boundary: :unknown
:weight: 1
-- :name: propagate_service_template
+- :name: propagate_instance_level_service
:feature_category: :source_code_management
:has_external_dependencies:
:latency_sensitive:
diff --git a/app/workers/propagate_instance_level_service_worker.rb b/app/workers/propagate_instance_level_service_worker.rb
new file mode 100644
index 00000000000..64ea61cabfa
--- /dev/null
+++ b/app/workers/propagate_instance_level_service_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Worker for updating any project specific caches.
+class PropagateInstanceLevelServiceWorker
+ include ApplicationWorker
+
+ feature_category :source_code_management
+
+ LEASE_TIMEOUT = 4.hours.to_i
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(instance_level_service_id)
+ return unless try_obtain_lease_for(instance_level_service_id)
+
+ Projects::PropagateInstanceLevelService.propagate(Service.find_by(id: instance_level_service_id))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def try_obtain_lease_for(instance_level_service_id)
+ Gitlab::ExclusiveLease
+ .new("propagate_instance_level_service_worker:#{instance_level_service_id}", timeout: LEASE_TIMEOUT)
+ .try_obtain
+ end
+end
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
deleted file mode 100644
index 73a2b453207..00000000000
--- a/app/workers/propagate_service_template_worker.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-# Worker for updating any project specific caches.
-class PropagateServiceTemplateWorker
- include ApplicationWorker
-
- feature_category :source_code_management
-
- LEASE_TIMEOUT = 4.hours.to_i
-
- # rubocop: disable CodeReuse/ActiveRecord
- def perform(template_id)
- return unless try_obtain_lease_for(template_id)
-
- Projects::PropagateServiceTemplate.propagate(Service.find_by(id: template_id))
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def try_obtain_lease_for(template_id)
- Gitlab::ExclusiveLease
- .new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT)
- .try_obtain
- end
-end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 0adf745c7ac..ba141f808a7 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -29,7 +29,15 @@ class RepositoryForkWorker
result = gitlab_shell.fork_repository(source_project, target_project)
- raise "Unable to fork project #{target_project.id} for repository #{source_project.disk_path} -> #{target_project.disk_path}" unless result
+ if result
+ link_lfs_objects(source_project, target_project)
+ else
+ raise_fork_failure(
+ source_project,
+ target_project,
+ 'Failed to create fork repository'
+ )
+ end
target_project.after_import
end
@@ -40,4 +48,20 @@ class RepositoryForkWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.") # rubocop:disable Gitlab/RailsLogger
false
end
+
+ def link_lfs_objects(source_project, target_project)
+ Projects::LfsPointers::LfsLinkService
+ .new(target_project)
+ .execute(source_project.lfs_objects_oids)
+ rescue Projects::LfsPointers::LfsLinkService::TooManyOidsError
+ raise_fork_failure(
+ source_project,
+ target_project,
+ 'Source project has too many LFS objects'
+ )
+ end
+
+ def raise_fork_failure(source_project, target_project, reason)
+ raise "Unable to fork project #{target_project.id} for repository #{source_project.disk_path} -> #{target_project.disk_path}: #{reason}"
+ end
end