Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-29 18:09:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-29 18:09:58 +0300
commit647de7e6fd971d435396cc8730a2d162240e3d7c (patch)
tree3e2fc4e6e8027375d6061f2ad4badda04ef04476
parent4233d3aa86fe94e6288279aa55d42ed95bfe753c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml1
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue159
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue17
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js11
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js6
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js15
-rw-r--r--app/controllers/boards/issues_controller.rb3
-rw-r--r--app/models/ci/instance_variable.rb17
-rw-r--r--app/services/boards/issues/list_service.rb9
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb18
-rw-r--r--changelogs/unreleased/14108-instance-level-ci-variables.yml5
-rw-r--r--changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml5
-rw-r--r--changelogs/unreleased/214882-render-single-panel.yml5
-rw-r--r--changelogs/unreleased/dblessing-remove-app-settings-redirect.yml5
-rw-r--r--changelogs/unreleased/error-tracking-link-target.yml5
-rw-r--r--changelogs/unreleased/jira-user-importer.yml5
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb2
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/routes/admin.rb4
-rw-r--r--db/migrate/20200422091541_create_ci_instance_variables.rb31
-rw-r--r--db/structure.sql30
-rw-r--r--doc/development/database_review.md8
-rw-r--r--doc/development/migration_style_guide.md26
-rw-r--r--lib/gitlab/jira_import/issue_serializer.rb50
-rw-r--r--lib/gitlab/usage_data.rb35
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa.rb6
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb68
-rw-r--r--qa/qa/resource/kubernetes_cluster/base.rb40
-rw-r--r--qa/qa/resource/kubernetes_cluster/project_cluster.rb72
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb2
-rwxr-xr-xscripts/regenerate-schema194
-rwxr-xr-xscripts/schema_changed.sh4
-rw-r--r--spec/factories/ci/instance_variables.rb13
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js55
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js112
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js41
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js22
-rw-r--r--spec/lib/gitlab/jira_import/issue_serializer_spec.rb116
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb72
-rw-r--r--spec/models/ci/instance_variable_spec.rb34
-rw-r--r--spec/support/helpers/usage_data_helpers.rb54
-rw-r--r--spec/workers/gitlab/jira_import/import_issue_worker_spec.rb19
47 files changed, 1190 insertions, 227 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 17c9d03eb98..ca254d499fb 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -143,6 +143,7 @@ db:migrate:reset:
db:check-schema:
extends: .db-job-base
script:
+ - scripts/regenerate-schema
- source scripts/schema_changed.sh
db:migrate-from-v11.11.0:
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 3d700f4d216..45432e8ebd8 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -393,9 +393,9 @@ export default {
<template #description>
<div>
<span>{{ __('Monitor your errors by integrating with Sentry.') }}</span>
- <a href="/help/user/project/operations/error_tracking.html">
- {{ __('More information') }}
- </a>
+ <gl-link target="_blank" href="/help/user/project/operations/error_tracking.html">{{
+ __('More information')
+ }}</gl-link>
</div>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 571da30675d..a77ee815b2c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -3,6 +3,8 @@ import { debounce, pickBy } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import {
+ GlIcon,
+ GlButton,
GlDeprecatedButton,
GlDropdown,
GlDropdownItem,
@@ -17,7 +19,6 @@ import {
import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
@@ -39,6 +40,8 @@ export default {
VueDraggable,
DashboardPanel,
Icon,
+ GlIcon,
+ GlButton,
GlDeprecatedButton,
GlDropdown,
GlLoadingIcon,
@@ -60,7 +63,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
externalDashboardUrl: {
type: String,
@@ -197,7 +199,6 @@ export default {
},
data() {
return {
- state: 'gettingStarted',
formIsValid: null,
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
hasValidDates: true,
@@ -212,8 +213,8 @@ export default {
'showEmptyState',
'useDashboardEndpoint',
'allDashboards',
- 'additionalPanelTypesEnabled',
'environmentsLoading',
+ 'expandedPanel',
]),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() {
@@ -232,14 +233,6 @@ export default {
this.firstDashboard === this.selectedDashboard
);
},
- hasHeaderButtons() {
- return (
- this.addingMetricsAvailable ||
- this.showRearrangePanelsBtn ||
- this.selectedDashboard.can_edit ||
- this.externalDashboardUrl.length
- );
- },
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
},
@@ -273,6 +266,8 @@ export default {
'setInitialState',
'setPanelGroupMetrics',
'filterEnvironments',
+ 'setExpandedPanel',
+ 'clearExpandedPanel',
]),
updatePanels(key, panels) {
this.setPanelGroupMetrics({
@@ -300,9 +295,13 @@ export default {
this.selectedTimeRange = defaultTimeRange;
},
- generateLink(group, title, yLabel) {
+ generatePanelLink(group, graphData) {
+ if (!group || !graphData) {
+ return null;
+ }
const dashboard = this.currentDashboard || this.firstDashboard.path;
- const params = pickBy({ dashboard, group, title, y_label: yLabel }, value => value != null);
+ const { y_label, title } = graphData;
+ const params = pickBy({ dashboard, group, title, y_label }, value => value != null);
return mergeUrlParams(params, window.location.href);
},
hideAddMetricModal() {
@@ -366,11 +365,20 @@ export default {
});
this.selectedTimeRange = { start, end };
},
+ onExpandPanel(group, panel) {
+ this.setExpandedPanel({ group, panel });
+ },
+ onGoBack() {
+ this.clearExpandedPanel();
+ },
},
addMetric: {
title: s__('Metrics|Add metric'),
modalId: 'add-metric',
},
+ i18n: {
+ goBackLabel: s__('Metrics|Go back'),
+ },
};
</script>
@@ -541,59 +549,88 @@ export default {
</div>
<div v-if="!showEmptyState">
- <graph-group
- v-for="(groupData, index) in dashboard.panelGroups"
- :key="`${groupData.group}.${groupData.priority}`"
- :name="groupData.group"
- :show-panels="showPanels"
- :collapse-group="collapseGroup(groupData.key)"
+ <dashboard-panel
+ v-show="expandedPanel.panel"
+ ref="expandedPanel"
+ :clipboard-text="generatePanelLink(expandedPanel.group, expandedPanel.panel)"
+ :graph-data="expandedPanel.panel"
+ :alerts-endpoint="alertsEndpoint"
+ :height="600"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ @timerangezoom="onTimeRangeZoom"
>
- <vue-draggable
- v-if="!groupSingleEmptyState(groupData.key)"
- :value="groupData.panels"
- group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
- :disabled="!isRearrangingPanels"
- @input="updatePanels(groupData.key, $event)"
+ <template #topLeft>
+ <gl-button
+ ref="goBackBtn"
+ v-gl-tooltip
+ class="mr-3 my-3"
+ :title="$options.i18n.goBackLabel"
+ @click="onGoBack"
+ >
+ <gl-icon
+ name="arrow-left"
+ :aria-label="$options.i18n.goBackLabel"
+ class="text-secondary"
+ />
+ </gl-button>
+ </template>
+ </dashboard-panel>
+
+ <div v-show="!expandedPanel.panel">
+ <graph-group
+ v-for="groupData in dashboard.panelGroups"
+ :key="`${groupData.group}.${groupData.priority}`"
+ :name="groupData.group"
+ :show-panels="showPanels"
+ :collapse-group="collapseGroup(groupData.key)"
>
- <div
- v-for="(graphData, graphIndex) in groupData.panels"
- :key="`dashboard-panel-${graphIndex}`"
- class="col-12 col-lg-6 px-2 mb-2 draggable"
- :class="{ 'draggable-enabled': isRearrangingPanels }"
+ <vue-draggable
+ v-if="!groupSingleEmptyState(groupData.key)"
+ :value="groupData.panels"
+ group="metrics-dashboard"
+ :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :disabled="!isRearrangingPanels"
+ @input="updatePanels(groupData.key, $event)"
>
- <div class="position-relative draggable-panel js-draggable-panel">
- <div
- v-if="isRearrangingPanels"
- class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
- @click="removePanel(groupData.key, groupData.panels, graphIndex)"
- >
- <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
- <icon name="close" />
- </a>
- </div>
+ <div
+ v-for="(graphData, graphIndex) in groupData.panels"
+ :key="`dashboard-panel-${graphIndex}`"
+ class="col-12 col-lg-6 px-2 mb-2 draggable"
+ :class="{ 'draggable-enabled': isRearrangingPanels }"
+ >
+ <div class="position-relative draggable-panel js-draggable-panel">
+ <div
+ v-if="isRearrangingPanels"
+ class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
+ @click="removePanel(groupData.key, groupData.panels, graphIndex)"
+ >
+ <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
+ <icon name="close" />
+ </a>
+ </div>
- <dashboard-panel
- :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
- :graph-data="graphData"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
- :index="`${index}-${graphIndex}`"
- @timerangezoom="onTimeRangeZoom"
- />
+ <dashboard-panel
+ :clipboard-text="generatePanelLink(groupData.group, graphData)"
+ :graph-data="graphData"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ @timerangezoom="onTimeRangeZoom"
+ @expand="onExpandPanel(groupData.group, graphData)"
+ />
+ </div>
</div>
+ </vue-draggable>
+ <div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
+ <group-empty-state
+ ref="empty-group"
+ :documentation-path="documentationPath"
+ :settings-path="settingsPath"
+ :selected-state="groupSingleEmptyState(groupData.key)"
+ :svg-path="emptyNoDataSmallSvgPath"
+ />
</div>
- </vue-draggable>
- <div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
- <group-empty-state
- ref="empty-group"
- :documentation-path="documentationPath"
- :settings-path="settingsPath"
- :selected-state="groupSingleEmptyState(groupData.key)"
- :svg-path="emptyNoDataSmallSvgPath"
- />
- </div>
- </graph-group>
+ </graph-group>
+ </div>
</div>
<empty-state
v-else
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 2b1791ad3e8..29d6a2a46c5 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -59,7 +59,8 @@ export default {
},
graphData: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
groupId: {
type: String,
@@ -114,17 +115,13 @@ export default {
},
}),
title() {
- return this.graphData.title || '';
+ return this.graphData?.title || '';
},
graphDataHasResult() {
- return (
- this.graphData.metrics &&
- this.graphData.metrics[0].result &&
- this.graphData.metrics[0].result.length > 0
- );
+ return this.graphData?.metrics?.[0]?.result?.length > 0;
},
graphDataIsLoading() {
- const { metrics = [] } = this.graphData;
+ const metrics = this.graphData?.metrics || [];
return metrics.some(({ loading }) => loading);
},
logsPathWithTimeRange() {
@@ -136,7 +133,7 @@ export default {
return null;
},
csvText() {
- const chartData = this.graphData.metrics[0].result[0].values;
+ const chartData = this.graphData?.metrics[0].result[0].values || [];
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
return chartData.reduce((csv, data) => {
@@ -230,7 +227,7 @@ export default {
return Object.values(this.getGraphAlerts(queries));
},
isPanelType(type) {
- return this.graphData.type && this.graphData.type === type;
+ return this.graphData?.type === type;
},
showToast() {
this.$toast.show(__('Link copied'));
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index f04f775761c..717f4cd9d66 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -89,6 +89,17 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
+export const setExpandedPanel = ({ commit }, { group, panel }) => {
+ commit(types.SET_EXPANDED_PANEL, { group, panel });
+};
+
+export const clearExpandedPanel = ({ commit }) => {
+ commit(types.SET_EXPANDED_PANEL, {
+ group: null,
+ panel: null,
+ });
+};
+
// All Data
export const fetchData = ({ dispatch }) => {
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 27a9a67edaa..f868fc4d40b 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -31,5 +31,5 @@ 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_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
-
export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER';
+export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index aa31b6642d7..5efca8215e4 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -134,6 +134,8 @@ export default {
metric.loading = false;
metric.result = null;
},
+
+ // Parameters and other information
[types.SET_INITIAL_STATE](state, initialState = {}) {
Object.assign(state, pick(initialState, initialStateKeys));
},
@@ -163,4 +165,8 @@ export default {
[types.SET_ENVIRONMENTS_FILTER](state, searchTerm) {
state.environmentsSearchTerm = searchTerm;
},
+ [types.SET_EXPANDED_PANEL](state, { group, panel }) {
+ state.expandedPanel.group = group;
+ state.expandedPanel.panel = panel;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index e60510e747b..af116f6b98f 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -17,6 +17,21 @@ export default () => ({
dashboard: {
panelGroups: [],
},
+ /**
+ * Panel that is currently "zoomed" in as
+ * a single panel in view.
+ */
+ expandedPanel: {
+ /**
+ * {?String} Panel's group name.
+ */
+ group: null,
+ /**
+ * {?Object} Panel content from `dashboard`
+ * null when no panel is expanded.
+ */
+ panel: null,
+ },
allDashboards: [],
// Other project data
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 1bfff210ecf..a18c80b996e 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -20,9 +20,6 @@ module Boards
skip_before_action :authenticate_user!, only: [:index]
before_action :validate_id_list, only: [:bulk_move]
before_action :can_move_issues?, only: [:bulk_move]
- before_action do
- push_frontend_feature_flag(:board_search_optimization, board.group, default_enabled: true)
- end
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
new file mode 100644
index 00000000000..19511696509
--- /dev/null
+++ b/app/models/ci/instance_variable.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Ci
+ class InstanceVariable < ApplicationRecord
+ extend Gitlab::Ci::Model
+ include Ci::NewHasVariable
+ include Ci::Maskable
+
+ alias_attribute :secret_value, :value
+
+ validates :key, uniqueness: {
+ message: "(%{value}) has already been taken"
+ }
+
+ scope :unprotected, -> { where(protected: false) }
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 9637eb1b918..e08509b84db 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -12,7 +12,7 @@ module Boards
def execute
return fetch_issues.order_closed_date_desc if list&.closed?
- fetch_issues.order_by_position_and_priority(with_cte: can_attempt_search_optimization?)
+ fetch_issues.order_by_position_and_priority(with_cte: params[:search].present?)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -91,7 +91,7 @@ module Boards
end
def set_attempt_search_optimizations
- return unless can_attempt_search_optimization?
+ return unless params[:search].present?
if board.group_board?
params[:attempt_group_search_optimizations] = true
@@ -130,11 +130,6 @@ module Boards
def board_group
board.group_board? ? parent : parent.group
end
-
- def can_attempt_search_optimization?
- params[:search].present? &&
- Feature.enabled?(:board_search_optimization, board_group, default_enabled: true)
- end
end
end
end
diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb
index 7ace0a35fd9..b3031256445 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -28,19 +28,25 @@ module Gitlab
private
def create_issue(issue_attributes, project_id)
+ label_ids = issue_attributes.delete('label_ids')
issue_id = insert_and_return_id(issue_attributes, Issue)
- label_issue(project_id, issue_id)
+ label_issue(project_id, issue_id, label_ids)
issue_id
end
- def label_issue(project_id, issue_id)
- label_id = JiraImport.get_import_label_id(project_id)
- return unless label_id
+ def label_issue(project_id, issue_id, label_ids)
+ label_link_attrs = label_ids.to_a.map do |label_id|
+ build_label_attrs(issue_id, label_id.to_i)
+ end
- label_link_attrs = build_label_attrs(issue_id, label_id.to_i)
- insert_and_return_id(label_link_attrs, LabelLink)
+ import_label_id = JiraImport.get_import_label_id(project_id)
+ return unless import_label_id
+
+ label_link_attrs << build_label_attrs(issue_id, import_label_id.to_i)
+
+ Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs)
end
def build_label_attrs(issue_id, label_id)
diff --git a/changelogs/unreleased/14108-instance-level-ci-variables.yml b/changelogs/unreleased/14108-instance-level-ci-variables.yml
new file mode 100644
index 00000000000..16de371282c
--- /dev/null
+++ b/changelogs/unreleased/14108-instance-level-ci-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Add migrations for global CI variables
+merge_request: 30156
+author:
+type: added
diff --git a/changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml b/changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml
new file mode 100644
index 00000000000..7ebf5f61f4b
--- /dev/null
+++ b/changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml
@@ -0,0 +1,5 @@
+---
+title: Collect object store config in usage data
+merge_request: 29149
+author:
+type: added
diff --git a/changelogs/unreleased/214882-render-single-panel.yml b/changelogs/unreleased/214882-render-single-panel.yml
new file mode 100644
index 00000000000..841b3ec92a9
--- /dev/null
+++ b/changelogs/unreleased/214882-render-single-panel.yml
@@ -0,0 +1,5 @@
+---
+title: View a details of a panel in 'full screen mode'
+merge_request: 29902
+author:
+type: added
diff --git a/changelogs/unreleased/dblessing-remove-app-settings-redirect.yml b/changelogs/unreleased/dblessing-remove-app-settings-redirect.yml
new file mode 100644
index 00000000000..1c232e60eee
--- /dev/null
+++ b/changelogs/unreleased/dblessing-remove-app-settings-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated /admin/application_settings redirect
+merge_request: 30532
+author:
+type: removed
diff --git a/changelogs/unreleased/error-tracking-link-target.yml b/changelogs/unreleased/error-tracking-link-target.yml
new file mode 100644
index 00000000000..2bb1fbb368c
--- /dev/null
+++ b/changelogs/unreleased/error-tracking-link-target.yml
@@ -0,0 +1,5 @@
+---
+title: Error tracking target blank empty state
+merge_request: 30525
+author:
+type: other
diff --git a/changelogs/unreleased/jira-user-importer.yml b/changelogs/unreleased/jira-user-importer.yml
new file mode 100644
index 00000000000..b4a9f7af594
--- /dev/null
+++ b/changelogs/unreleased/jira-user-importer.yml
@@ -0,0 +1,5 @@
+---
+title: Map Jira issue assignee and author
+merge_request: 30498
+author:
+type: added
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index 714dd505824..09c9b325a04 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -45,7 +45,7 @@ module GettextI18nRailsJs
private
def gettext_messages_by_file
- @gettext_messages_by_file ||= JSON.parse(load_messages)
+ @gettext_messages_by_file ||= Gitlab::Json.parse(load_messages)
end
def load_messages
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index cc45ee3c706..eb074b72125 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -75,7 +75,7 @@ Sidekiq.configure_server do |config|
# Sidekiq-cron: load recurring jobs from gitlab.yml
# UGLY Hack to get nested hash from settingslogic
- cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
+ cron_jobs = Gitlab::Json.parse(Gitlab.config.cron_jobs.to_json)
# UGLY hack: Settingslogic doesn't allow 'class' key
cron_jobs_required_keys = %w(job_class cron)
cron_jobs.each do |k, v|
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 96cd6e5f587..8ba6a6a09e2 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -116,10 +116,6 @@ namespace :admin do
end
resource :application_settings, only: :update do
- # This redirect should be removed with 13.0 release.
- # https://gitlab.com/gitlab-org/gitlab/issues/199427
- get '/', to: redirect('admin/application_settings/general'), as: nil
-
resources :services, only: [:index, :edit, :update]
resources :integrations, only: [:edit, :update] do
member do
diff --git a/db/migrate/20200422091541_create_ci_instance_variables.rb b/db/migrate/20200422091541_create_ci_instance_variables.rb
new file mode 100644
index 00000000000..ab2a4722f89
--- /dev/null
+++ b/db/migrate/20200422091541_create_ci_instance_variables.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreateCiInstanceVariables < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:ci_instance_variables)
+ create_table :ci_instance_variables do |t|
+ t.integer :variable_type, null: false, limit: 2, default: 1
+ t.boolean :masked, default: false, allow_null: false
+ t.boolean :protected, default: false, allow_null: false
+ t.text :key, null: false
+ t.text :encrypted_value
+ t.text :encrypted_value_iv
+
+ t.index [:key], name: 'index_ci_instance_variables_on_key', unique: true, using: :btree
+ end
+ end
+
+ add_text_limit(:ci_instance_variables, :key, 255)
+ add_text_limit(:ci_instance_variables, :encrypted_value, 1024)
+ add_text_limit(:ci_instance_variables, :encrypted_value_iv, 255)
+ end
+
+ def down
+ drop_table :ci_instance_variables
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 4a0776620a1..d70c3b86704 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1069,6 +1069,28 @@ CREATE SEQUENCE public.ci_group_variables_id_seq
ALTER SEQUENCE public.ci_group_variables_id_seq OWNED BY public.ci_group_variables.id;
+CREATE TABLE public.ci_instance_variables (
+ id bigint NOT NULL,
+ variable_type smallint DEFAULT 1 NOT NULL,
+ masked boolean DEFAULT false,
+ protected boolean DEFAULT false,
+ key text NOT NULL,
+ encrypted_value text,
+ encrypted_value_iv text,
+ CONSTRAINT check_07a45a5bcb CHECK ((char_length(encrypted_value_iv) <= 255)),
+ CONSTRAINT check_5aede12208 CHECK ((char_length(key) <= 255)),
+ CONSTRAINT check_5ebd0515a0 CHECK ((char_length(encrypted_value) <= 1024))
+);
+
+CREATE SEQUENCE public.ci_instance_variables_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.ci_instance_variables_id_seq OWNED BY public.ci_instance_variables.id;
+
CREATE TABLE public.ci_job_artifacts (
id integer NOT NULL,
project_id integer NOT NULL,
@@ -7227,6 +7249,8 @@ ALTER TABLE ONLY public.ci_daily_report_results ALTER COLUMN id SET DEFAULT next
ALTER TABLE ONLY public.ci_group_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_group_variables_id_seq'::regclass);
+ALTER TABLE ONLY public.ci_instance_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_instance_variables_id_seq'::regclass);
+
ALTER TABLE ONLY public.ci_job_artifacts ALTER COLUMN id SET DEFAULT nextval('public.ci_job_artifacts_id_seq'::regclass);
ALTER TABLE ONLY public.ci_job_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_job_variables_id_seq'::regclass);
@@ -7892,6 +7916,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.ci_group_variables
ADD CONSTRAINT ci_group_variables_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.ci_instance_variables
+ ADD CONSTRAINT ci_instance_variables_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id);
@@ -9073,6 +9100,8 @@ CREATE INDEX index_ci_daily_report_results_on_last_pipeline_id ON public.ci_dail
CREATE UNIQUE INDEX index_ci_group_variables_on_group_id_and_key ON public.ci_group_variables USING btree (group_id, key);
+CREATE UNIQUE INDEX index_ci_instance_variables_on_key ON public.ci_instance_variables USING btree (key);
+
CREATE INDEX index_ci_job_artifacts_file_store_is_null ON public.ci_job_artifacts USING btree (id) WHERE (file_store IS NULL);
CREATE INDEX index_ci_job_artifacts_on_expire_at_and_job_id ON public.ci_job_artifacts USING btree (expire_at, job_id);
@@ -13556,6 +13585,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200420201933
20200421092907
20200421233150
+20200422091541
20200422213749
20200423075720
20200423080334
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index f2db0ab4fd5..aa7ebb3756f 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -74,12 +74,12 @@ the following preparations into account.
#### Preparation when adding migrations
-- Ensure `db/structure.sql` is updated.
+- Ensure `db/structure.sql` is updated as [documented](migration_style_guide.md#schema-changes).
- Make migrations reversible by using the `change` method or include a `down` method when using `up`.
- Include either a rollback procedure or describe how to rollback changes.
-- Add the output of both migrating and rolling back for all migrations into the MR description
- - Ensure the down method reverts the changes in `db/structure.sql`
- - Update the migration output whenever you modify the migrations during the review process
+- Add the output of both migrating and rolling back for all migrations into the MR description.
+ - Ensure the down method reverts the changes in `db/structure.sql`.
+ - Update the migration output whenever you modify the migrations during the review process.
- Add tests for the migration in `spec/migrations` if necessary. See [Testing Rails migrations at GitLab](testing_guide/testing_migrations_guide.md) for more details.
- When [high-traffic](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/migration_helpers.rb#L12) tables are involved in the migration, use the [`with_lock_retries`](migration_style_guide.md#retry-mechanism-when-acquiring-database-locks) helper method. Review the relevant [examples in our documentation](migration_style_guide.md#examples) for use cases and solutions.
- Ensure RuboCop checks are not disabled unless there's a valid reason to.
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 3e993243855..371fdf8b7f0 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -35,9 +35,29 @@ and post-deployment migrations (`db/post_migrate`) are run after the deployment
## Schema Changes
-Migrations that make changes to the database schema (e.g. adding a column) can
-only be added in the monthly release, patch releases may only contain data
-migrations _unless_ schema changes are absolutely required to solve a problem.
+Changes to the schema should be commited to `db/structure.sql`. This
+file is automatically generated by Rails, so you normally should not
+edit this file by hand. If your migration is adding a column to a
+table, that column will be added at the bottom. Please do not reorder
+columns manually for existing tables as this will cause confusing to
+other people using `db/structure.sql` generated by Rails.
+
+When your local database in your GDK is diverging from the schema from
+`master` it might be hard to cleanly commit the schema changes to
+Git. In that case you can use the `script/regenerate-schema` script to
+regenerate a clean `db/structure.sql` for the migrations you're
+adding. This script will apply all migrations found in `db/migrate`
+or `db/post_migrate`, so if there are any migrations you don't want to
+commit to the schema, rename or remove them. If your branch is not
+targetting `master` you can set the `TARGET` environment variable.
+
+```sh
+# Regenerate schema against `master`
+bin/regenerate-schema
+
+# Regenerate schema against `12-9-stable-ee`
+TARGET=12-9-stable-ee bin/regenerate-schema
+```
## What Requires Downtime?
diff --git a/lib/gitlab/jira_import/issue_serializer.rb b/lib/gitlab/jira_import/issue_serializer.rb
index f05b36e54da..244c9fd637d 100644
--- a/lib/gitlab/jira_import/issue_serializer.rb
+++ b/lib/gitlab/jira_import/issue_serializer.rb
@@ -21,7 +21,8 @@ module Gitlab
state_id: map_status(jira_issue.status.statusCategory),
updated_at: jira_issue.updated,
created_at: jira_issue.created,
- author_id: project.creator_id, # TODO: map actual author: https://gitlab.com/gitlab-org/gitlab/-/issues/210580
+ author_id: reporter,
+ assignee_ids: assignees,
label_ids: label_ids
}
end
@@ -34,8 +35,6 @@ module Gitlab
def description
body = []
- body << formatter.author_line(jira_issue.reporter.displayName)
- body << formatter.assignee_line(jira_issue.assignee.displayName) if jira_issue.assignee
body << jira_issue.description
body << MetadataCollector.new(jira_issue).execute
@@ -51,6 +50,38 @@ module Gitlab
end
end
+ def map_user_id(email)
+ return unless email
+
+ # We also include emails that are not yet confirmed
+ users = User.by_any_email(email).to_a
+
+ # this event should never happen but we should log it in case we have invalid data
+ log_user_mapping_message('Multiple users found for an email address', email) if users.count > 1
+
+ user = users.first
+
+ unless project.project_member(user)
+ log_user_mapping_message('Jira user not found', email)
+
+ return
+ end
+
+ user.id
+ end
+
+ def reporter
+ map_user_id(jira_issue&.reporter&.emailAddress) || project.creator_id
+ end
+
+ def assignees
+ found_user_id = map_user_id(jira_issue&.assignee&.emailAddress)
+
+ return unless found_user_id
+
+ [found_user_id]
+ end
+
# We already create labels in Gitlab::JiraImport::LabelsImporter stage but
# there is a possibility it may fail or
# new labels were created on the Jira in the meantime
@@ -59,6 +90,19 @@ module Gitlab
Gitlab::JiraImport::HandleLabelsService.new(project, jira_issue.fields['labels']).execute
end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+
+ def log_user_mapping_message(message, email)
+ logger.info(
+ project_id: project.id,
+ project_path: project.full_path,
+ user_email: email,
+ message: message
+ )
+ end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index f8ee0ca6877..2272803fcfa 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -24,6 +24,7 @@ module Gitlab
.merge(features_usage_data)
.merge(components_usage_data)
.merge(cycle_analytics_usage_data)
+ .merge(object_store_usage_data)
end
def to_json(force_refresh: false)
@@ -237,6 +238,40 @@ module Gitlab
'unknown_app_server_type'
end
+ def object_store_config(component)
+ config = alt_usage_data(fallback: nil) do
+ Settings[component]['object_store']
+ end
+
+ if config
+ {
+ enabled: alt_usage_data { Settings[component]['enabled'] },
+ object_store: {
+ enabled: alt_usage_data { config['enabled'] },
+ direct_upload: alt_usage_data { config['direct_upload'] },
+ background_upload: alt_usage_data { config['background_upload'] },
+ provider: alt_usage_data { config['connection']['provider'] }
+ }
+ }
+ else
+ {
+ enabled: alt_usage_data { Settings[component]['enabled'] }
+ }
+ end
+ end
+
+ def object_store_usage_data
+ {
+ object_store: {
+ artifacts: object_store_config('artifacts'),
+ external_diffs: object_store_config('external_diffs'),
+ lfs: object_store_config('lfs'),
+ uploads: object_store_config('uploads'),
+ packages: object_store_config('packages')
+ }
+ }
+ end
+
def ingress_modsecurity_usage
::Clusters::Applications::IngressModsecurityUsageService.new.execute
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d55a544944d..356354ff52f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13129,6 +13129,9 @@ msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index 378ea79ce45..ac0886dbd90 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -75,7 +75,6 @@ module QA
autoload :CiVariable, 'qa/resource/ci_variable'
autoload :Runner, 'qa/resource/runner'
autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
- autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster'
autoload :User, 'qa/resource/user'
autoload :ProjectMilestone, 'qa/resource/project_milestone'
autoload :Members, 'qa/resource/members'
@@ -89,6 +88,11 @@ module QA
autoload :UserGPG, 'qa/resource/user_gpg'
autoload :Visibility, 'qa/resource/visibility'
+ module KubernetesCluster
+ autoload :Base, 'qa/resource/kubernetes_cluster/base'
+ autoload :ProjectCluster, 'qa/resource/kubernetes_cluster/project_cluster'
+ end
+
module Events
autoload :Base, 'qa/resource/events/base'
autoload :Project, 'qa/resource/events/project'
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
deleted file mode 100644
index 7306acfe2a4..00000000000
--- a/qa/qa/resource/kubernetes_cluster.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require 'securerandom'
-
-module QA
- module Resource
- class KubernetesCluster < Base
- attr_writer :project, :cluster,
- :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain
-
- attribute :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
- end
-
- def fabricate!
- @project.visit!
-
- Page::Project::Menu.perform(
- &:go_to_operations_kubernetes)
-
- Page::Project::Operations::Kubernetes::Index.perform(
- &:add_kubernetes_cluster)
-
- Page::Project::Operations::Kubernetes::Add.perform(
- &:add_existing_cluster)
-
- Page::Project::Operations::Kubernetes::AddExisting.perform do |cluster_page|
- cluster_page.set_cluster_name(@cluster.cluster_name)
- cluster_page.set_api_url(@cluster.api_url)
- cluster_page.set_ca_certificate(@cluster.ca_certificate)
- cluster_page.set_token(@cluster.token)
- cluster_page.uncheck_rbac! unless @cluster.rbac
- cluster_page.add_cluster!
- end
-
- if @install_helm_tiller
- Page::Project::Operations::Kubernetes::Show.perform do |show|
- # We must wait a few seconds for permissions to be set up correctly for new cluster
- sleep 10
-
- # Open applications tab
- show.open_applications
-
- # Helm must be installed before everything else
- show.install!(:helm)
- show.await_installed(:helm)
-
- show.install!(:ingress) if @install_ingress
- show.install!(:prometheus) if @install_prometheus
- show.install!(:runner) if @install_runner
-
- show.await_installed(:ingress) if @install_ingress
- show.await_installed(:prometheus) if @install_prometheus
- show.await_installed(:runner) if @install_runner
-
- if @install_ingress
- populate(:ingress_ip)
-
- show.open_details
- show.set_domain("#{ingress_ip}.nip.io")
- show.save_domain
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/resource/kubernetes_cluster/base.rb b/qa/qa/resource/kubernetes_cluster/base.rb
new file mode 100644
index 00000000000..38bca48be17
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster/base.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ module KubernetesCluster
+ class Base < Resource::Base
+ attr_writer :add_name_uuid
+
+ attribute :id
+ attribute :name
+ attribute :domain
+ attribute :enabled
+ attribute :managed
+ attribute :management_project_id
+
+ attribute :api_url
+ attribute :token
+ attribute :ca_cert
+ attribute :namespace
+
+ attribute :authorization_type
+ attribute :environment_scope
+
+ def initialize
+ @add_name_uuid = true
+ @enabled = true
+ @managed = true
+ @authorization_type = :rbac
+ @environment_scope = :*
+ end
+
+ def name=(new_name)
+ @name = @add_name_uuid ? "#{new_name}-#{SecureRandom.hex(5)}" : new_name
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
new file mode 100644
index 00000000000..5c61cc29236
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ module KubernetesCluster
+ class ProjectCluster < Base
+ attr_writer :cluster,
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain
+
+ attribute :project do
+ Resource::Project.fabricate!
+ end
+
+ attribute :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(
+ &:go_to_operations_kubernetes)
+
+ Page::Project::Operations::Kubernetes::Index.perform(
+ &:add_kubernetes_cluster)
+
+ Page::Project::Operations::Kubernetes::Add.perform(
+ &:add_existing_cluster)
+
+ Page::Project::Operations::Kubernetes::AddExisting.perform do |cluster_page|
+ cluster_page.set_cluster_name(@cluster.cluster_name)
+ cluster_page.set_api_url(@cluster.api_url)
+ cluster_page.set_ca_certificate(@cluster.ca_certificate)
+ cluster_page.set_token(@cluster.token)
+ cluster_page.uncheck_rbac! unless @cluster.rbac
+ cluster_page.add_cluster!
+ end
+
+ if @install_helm_tiller
+ Page::Project::Operations::Kubernetes::Show.perform do |show|
+ # We must wait a few seconds for permissions to be set up correctly for new cluster
+ sleep 10
+
+ # Open applications tab
+ show.open_applications
+
+ # Helm must be installed before everything else
+ show.install!(:helm)
+ show.await_installed(:helm)
+
+ show.install!(:ingress) if @install_ingress
+ show.install!(:prometheus) if @install_prometheus
+ show.install!(:runner) if @install_runner
+
+ show.await_installed(:ingress) if @install_ingress
+ show.await_installed(:prometheus) if @install_prometheus
+ show.await_installed(:runner) if @install_runner
+
+ if @install_ingress
+ populate(:ingress_ip)
+
+ show.open_details
+ show.set_domain("#{ingress_ip}.nip.io")
+ show.save_domain
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 0a52b01af03..292fc40bec4 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -35,7 +35,7 @@ module QA
end
# Connect K8s cluster
- Resource::KubernetesCluster.fabricate! do |k8s_cluster|
+ Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
k8s_cluster.install_helm_tiller = true
diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
index 9a52109c8cb..ab6c08f8ec5 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
@@ -21,12 +21,10 @@ module QA
end
it 'can create and associate a project cluster', :smoke do
- Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster|
+ Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
- end
-
- project.visit!
+ end.project.visit!
Page::Project::Menu.perform(&:go_to_operations_kubernetes)
diff --git a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb
index f7463c69db1..465b3530d00 100644
--- a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb
+++ b/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb
@@ -69,7 +69,7 @@ module QA
project.description = 'Cluster with Prometheus'
end
- @cluster_props = Resource::KubernetesCluster.fabricate_via_browser_ui! do |cluster_settings|
+ @cluster_props = Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |cluster_settings|
cluster_settings.project = @project
cluster_settings.cluster = @cluster
cluster_settings.install_helm_tiller = true
diff --git a/scripts/regenerate-schema b/scripts/regenerate-schema
new file mode 100755
index 00000000000..b63a75cdc83
--- /dev/null
+++ b/scripts/regenerate-schema
@@ -0,0 +1,194 @@
+#!/usr/bin/env ruby
+
+# frozen_string_literal: true
+
+require 'net/http'
+require 'uri'
+
+class SchemaRegenerator
+ ##
+ # Filename of the schema
+ #
+ # This file is being regenerated by this script.
+ FILENAME = 'db/structure.sql'
+
+ ##
+ # Directories where migrations are stored
+ #
+ # The methods +hide_migrations+ and +unhide_migrations+ will rename
+ # these to disable/enable migrations.
+ MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
+
+ def execute
+ Dir.chdir(File.expand_path('..', __dir__)) do
+ checkout_ref
+ checkout_clean_schema
+ hide_migrations
+ reset_db
+ unhide_migrations
+ migrate
+ ensure
+ unhide_migrations
+ end
+ end
+
+ private
+
+ ##
+ # Git checkout +CI_COMMIT_SHA+.
+ #
+ # When running from CI, checkout the clean commit,
+ # not the merged result.
+ def checkout_ref
+ return unless ci?
+
+ run %Q[git checkout #{source_ref}]
+ run %q[git clean -f -- db]
+ end
+
+ ##
+ # Checkout the clean schema from the target branch
+ def checkout_clean_schema
+ remote_checkout_clean_schema || local_checkout_clean_schema
+ end
+
+ ##
+ # Get clean schema from remote servers
+ #
+ # This script might run in CI, using a shallow clone, so to checkout
+ # the file, download it from the server.
+ def remote_checkout_clean_schema
+ return false unless project_url
+
+ uri = URI.join("#{project_url}/", 'raw/', "#{merge_base}/", FILENAME)
+
+ download_schema(uri)
+ end
+
+ ##
+ # Download the schema from the given +uri+.
+ def download_schema(uri)
+ puts "Downloading #{uri}..."
+
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ request = Net::HTTP::Get.new(uri.request_uri)
+ http.read_timeout = 500
+ http.request(request) do |response|
+ raise("Failed to download file: #{response.code} #{response.message}") if response.code.to_i != 200
+
+ File.open(FILENAME, 'w') do |io|
+ response.read_body do |chunk|
+ io.write(chunk)
+ end
+ end
+ end
+ end
+
+ true
+ end
+
+ ##
+ # Git checkout the schema from target branch.
+ #
+ # Ask git to checkout the schema from the target branch and reset
+ # the file to unstage the changes.
+ def local_checkout_clean_schema
+ run %Q[git checkout #{merge_base} -- #{FILENAME}]
+ run %Q[git reset -- #{FILENAME}]
+ end
+
+ ##
+ # Move migrations to where Rails will not find them.
+ #
+ # To reset the database to clean schema defined in +FILENAME+, move
+ # the migrations to a path where Rails will not find them, otherwise
+ # +db:reset+ would abort. Later when the migrations should be
+ # applied, use +unhide_migrations+ to bring them back.
+ def hide_migrations
+ MIGRATION_DIRS.each do |dir|
+ File.rename(dir, "#{dir}__")
+ end
+ end
+
+ ##
+ # Undo the effect of +hide_migrations+.
+ #
+ # Place back the migrations which might be moved by
+ # +hide_migrations+.
+ def unhide_migrations
+ error = nil
+
+ MIGRATION_DIRS.each do |dir|
+ File.rename("#{dir}__", dir)
+ rescue Errno::ENOENT
+ nil
+ rescue StandardError => e
+ # Save error for later, but continue with other dirs first
+ error = e
+ end
+
+ raise error if error
+ end
+
+ ##
+ # Run rake task to reset the database.
+ def reset_db
+ run %q[bin/rails db:reset RAILS_ENV=test]
+ end
+
+ ##
+ # Run rake task to run migrations.
+ def migrate
+ run %q[bin/rails db:migrate RAILS_ENV=test]
+ end
+
+ ##
+ # Run the given +cmd+.
+ #
+ # The command is colored green, and the output of the command is
+ # colored gray.
+ # When the command failed an exception is raised.
+ def run(cmd)
+ puts "\e[32m$ #{cmd}\e[37m"
+ ret = system(cmd)
+ puts "\e[0m"
+ raise("Command failed") unless ret
+ end
+
+ ##
+ # Return the base commit between source and target branch.
+ def merge_base
+ @merge_base ||= `git merge-base #{target_branch} #{source_ref}`.chomp
+ end
+
+ ##
+ # Return the name of the target branch
+ #
+ # Get source ref from CI environment variable, or read the +TARGET+
+ # environment+ variable, or default to +HEAD+.
+ def target_branch
+ ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || 'master'
+ end
+
+ ##
+ # Return the source ref
+ #
+ # Get source ref from CI environment variable, or default to +HEAD+.
+ def source_ref
+ ENV['CI_COMMIT_SHA'] || 'HEAD'
+ end
+
+ ##
+ # Return the project URL from CI environment variable.
+ def project_url
+ ENV['CI_PROJECT_URL']
+ end
+
+ ##
+ # Return whether the script is running from CI
+ def ci?
+ ENV['CI']
+ end
+end
+
+SchemaRegenerator.new.execute
diff --git a/scripts/schema_changed.sh b/scripts/schema_changed.sh
index e8c120e92e1..427e0128df7 100755
--- a/scripts/schema_changed.sh
+++ b/scripts/schema_changed.sh
@@ -2,13 +2,13 @@
schema_changed() {
if [ ! -z "$(git diff --name-only -- db/structure.sql)" ]; then
- printf "db/structure.sql after rake db:migrate:reset is different from one in the repository"
+ printf "Schema changes are not cleanly committed to db/structure.sql\n"
printf "The diff is as follows:\n"
diff=$(git diff -p --binary -- db/structure.sql)
printf "%s" "$diff"
exit 1
else
- printf "db/structure.sql after rake db:migrate:reset matches one in the repository"
+ printf "Schema changes are correctly applied to db/structure.sql\n"
fi
}
diff --git a/spec/factories/ci/instance_variables.rb b/spec/factories/ci/instance_variables.rb
new file mode 100644
index 00000000000..5a3551d3561
--- /dev/null
+++ b/spec/factories/ci/instance_variables.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_instance_variable, class: 'Ci::InstanceVariable' do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value { 'VARIABLE_VALUE' }
+ masked { false }
+
+ trait(:protected) do
+ add_attribute(:protected) { true }
+ end
+ end
+end
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index d440c063dd4..1eec2980f19 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -56,7 +56,7 @@ describe('Dashboard Panel', () => {
const findTitle = () => wrapper.find({ ref: 'graphTitle' });
const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' });
- const createWrapper = (props, options = {}) => {
+ const createWrapper = (props, options) => {
wrapper = shallowMount(DashboardPanel, {
propsData: {
graphData,
@@ -108,24 +108,51 @@ describe('Dashboard Panel', () => {
wrapper.destroy();
});
- describe('Empty Chart component', () => {
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphDataEmpty.title);
- });
+ it('renders the chart title', () => {
+ expect(findTitle().text()).toBe(graphDataEmpty.title);
+ });
- it('renders the no download csv link', () => {
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
+ it('renders no download csv link', () => {
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ });
- it('does not contain graph widgets', () => {
- expect(findContextualMenu().exists()).toBe(false);
- });
+ it('does not contain graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(false);
+ });
- it('is a Vue instance', () => {
- expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
- expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ it('The Empty Chart component is rendered and is a Vue instance', () => {
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
+ });
+
+ describe('When graphData is null', () => {
+ beforeEach(() => {
+ createWrapper({
+ graphData: null,
});
});
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders no chart title', () => {
+ expect(findTitle().text()).toBe('');
+ });
+
+ it('renders no download csv link', () => {
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ });
+
+ it('does not contain graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(false);
+ });
+
+ it('The Empty Chart component is rendered and is a Vue instance', () => {
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
});
describe('When graphData is available', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index be3c83dfd7d..6ac5248759b 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -212,6 +212,97 @@ describe('Dashboard', () => {
});
});
+ describe('single panel expands to "full screen" mode', () => {
+ const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
+
+ describe('when the panel is not expanded', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(wrapper.vm.$store);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('expanded panel is not visible', () => {
+ expect(findExpandedPanel().isVisible()).toBe(false);
+ });
+
+ it('can set a panel as expanded', () => {
+ const panel = wrapper.findAll(DashboardPanel).at(1);
+
+ jest.spyOn(store, 'dispatch');
+
+ panel.vm.$emit('expand');
+
+ const groupData = metricsDashboardViewModel.panelGroups[0];
+
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
+ group: groupData.group,
+ panel: expect.objectContaining({
+ id: groupData.panels[0].id,
+ }),
+ });
+ });
+ });
+
+ describe('when the panel is expanded', () => {
+ let group;
+ let panel;
+
+ const MockPanel = {
+ template: `<div><slot name="topLeft"/></div>`,
+ };
+
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
+ setupStoreWithData(wrapper.vm.$store);
+
+ const { panelGroups } = wrapper.vm.$store.state.monitoringDashboard.dashboard;
+
+ group = panelGroups[0].group;
+ [panel] = panelGroups[0].panels;
+
+ wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
+ group,
+ panel,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays a single panel and others are hidden', () => {
+ const panels = wrapper.findAll(MockPanel);
+ const visiblePanels = panels.filter(w => w.isVisible());
+
+ expect(findExpandedPanel().isVisible()).toBe(true);
+ // v-show for hiding panels is more performant than v-if
+ // check for panels to be hidden.
+ expect(panels.length).toBe(metricsDashboardPanelCount + 1);
+ expect(visiblePanels.length).toBe(1);
+ });
+
+ it('sets a link to the expanded panel', () => {
+ const searchQuery =
+ '?group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
+
+ expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
+ expect.stringContaining(searchQuery),
+ );
+ });
+
+ it('restores full dashboard by clicking `back`', () => {
+ const backBtn = wrapper.find({ ref: 'goBackBtn' });
+ expect(backBtn.exists()).toBe(true);
+
+ jest.spyOn(store, 'dispatch');
+ backBtn.vm.$emit('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/clearExpandedPanel',
+ undefined,
+ );
+ });
+ });
+ });
+
describe('when one of the metrics is missing', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
@@ -499,11 +590,12 @@ describe('Dashboard', () => {
describe('Clipboard text in panels', () => {
const currentDashboard = 'TEST_DASHBOARD';
+ const panelIndex = 1; // skip expanded panel
- const getClipboardTextAt = i =>
+ const getClipboardTextFirstPanel = () =>
wrapper
.findAll(DashboardPanel)
- .at(i)
+ .at(panelIndex)
.props('clipboardText');
beforeEach(() => {
@@ -515,18 +607,18 @@ describe('Dashboard', () => {
});
it('contains a link to the dashboard', () => {
- expect(getClipboardTextAt(0)).toContain(`dashboard=${currentDashboard}`);
- expect(getClipboardTextAt(0)).toContain(`group=`);
- expect(getClipboardTextAt(0)).toContain(`title=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
+ expect(getClipboardTextFirstPanel()).toContain(`dashboard=${currentDashboard}`);
+ expect(getClipboardTextFirstPanel()).toContain(`group=`);
+ expect(getClipboardTextFirstPanel()).toContain(`title=`);
+ expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
it('strips the undefined parameter', () => {
wrapper.setProps({ currentDashboard: undefined });
return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
+ expect(getClipboardTextFirstPanel()).not.toContain(`dashboard=`);
+ expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
});
@@ -534,8 +626,8 @@ describe('Dashboard', () => {
wrapper.setProps({ currentDashboard: null });
return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
+ expect(getClipboardTextFirstPanel()).not.toContain(`dashboard=`);
+ expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
});
});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 37bd270a1c0..9039dd3d6db 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -20,6 +20,8 @@ import {
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
+ setExpandedPanel,
+ clearExpandedPanel,
setGettingStartedEmptyState,
duplicateSystemDashboard,
} from '~/monitoring/stores/actions';
@@ -870,4 +872,43 @@ describe('Monitoring store actions', () => {
});
});
});
+
+ describe('setExpandedPanel', () => {
+ let state;
+
+ beforeEach(() => {
+ state = storeState();
+ });
+
+ it('Sets a panel as expanded', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
+
+ return testAction(
+ setExpandedPanel,
+ { group, panel },
+ state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
+ [],
+ );
+ });
+ });
+
+ describe('clearExpandedPanel', () => {
+ let state;
+
+ beforeEach(() => {
+ state = storeState();
+ });
+
+ it('Clears a panel as expanded', () => {
+ return testAction(
+ clearExpandedPanel,
+ undefined,
+ state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
+ [],
+ );
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 1452e9bc491..ab3debb798d 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -342,4 +342,26 @@ describe('Monitoring mutations', () => {
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
});
});
+
+ describe('SET_EXPANDED_PANEL', () => {
+ it('no expanded panel is set initally', () => {
+ expect(stateCopy.expandedPanel.panel).toEqual(null);
+ expect(stateCopy.expandedPanel.group).toEqual(null);
+ });
+
+ it('sets a panel id as the expanded panel', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
+ mutations[types.SET_EXPANDED_PANEL](stateCopy, { group, panel });
+
+ expect(stateCopy.expandedPanel).toEqual({ group, panel });
+ });
+
+ it('clears panel as the expanded panel', () => {
+ mutations[types.SET_EXPANDED_PANEL](stateCopy, { group: null, panel: null });
+
+ expect(stateCopy.expandedPanel.group).toEqual(null);
+ expect(stateCopy.expandedPanel.panel).toEqual(null);
+ });
+ });
});
diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
index 5c84556ab25..f24a83994f7 100644
--- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
@@ -16,7 +16,8 @@ describe Gitlab::JiraImport::IssueSerializer do
let(:description) { 'basic description' }
let(:created_at) { '2020-01-01 20:00:00' }
let(:updated_at) { '2020-01-10 20:00:00' }
- let(:assignee) { double(displayName: 'Solver') }
+ let(:assignee) { double(displayName: 'Solver', emailAddress: 'assignee@example.com') }
+ let(:reporter) { double(displayName: 'Reporter', emailAddress: 'reporter@example.com') }
let(:jira_status) { 'new' }
let(:parent_field) do
@@ -42,7 +43,7 @@ describe Gitlab::JiraImport::IssueSerializer do
created: created_at,
updated: updated_at,
assignee: assignee,
- reporter: double(displayName: 'Reporter'),
+ reporter: reporter,
status: double(statusCategory: { 'key' => jira_status }),
fields: fields
)
@@ -54,10 +55,6 @@ describe Gitlab::JiraImport::IssueSerializer do
let(:expected_description) do
<<~MD
- *Created by: Reporter*
-
- *Assigned to: Solver*
-
basic description
---
@@ -80,6 +77,7 @@ describe Gitlab::JiraImport::IssueSerializer do
updated_at: updated_at,
created_at: created_at,
author_id: project.creator_id,
+ assignee_ids: nil,
label_ids: [project_label.id, group_label.id] + Label.reorder(id: :asc).last(2).pluck(:id)
)
end
@@ -88,22 +86,108 @@ describe Gitlab::JiraImport::IssueSerializer do
expect(Issue.new(subject)).to be_valid
end
- it 'creates all missing labels (on project level)' do
- expect { subject }.to change { Label.count }.from(3).to(5)
+ context 'labels' do
+ it 'creates all missing labels (on project level)' do
+ expect { subject }.to change { Label.count }.from(3).to(5)
+
+ expect(Label.find_by(title: 'frontend').project).to eq(project)
+ expect(Label.find_by(title: 'backend').project).to eq(project)
+ end
+
+ context 'when there are no new labels' do
+ let(:labels_field) { %w(bug dev) }
- expect(Label.find_by(title: 'frontend').project).to eq(project)
- expect(Label.find_by(title: 'backend').project).to eq(project)
+ it 'assigns the labels to the Issue hash' do
+ expect(subject[:label_ids]).to match_array([project_label.id, group_label.id])
+ end
+
+ it 'does not create new labels' do
+ expect { subject }.not_to change { Label.count }.from(3)
+ end
+ end
end
- context 'when there are no new labels' do
- let(:labels_field) { %w(bug dev) }
+ context 'author' do
+ context 'when reporter maps to a GitLab user who is a project member' do
+ let!(:user) { create(:user, email: 'reporter@example.com') }
- it 'assigns the labels to the Issue hash' do
- expect(subject[:label_ids]).to match_array([project_label.id, group_label.id])
+ it 'sets the issue author to the mapped user' do
+ project.add_developer(user)
+
+ expect(subject[:author_id]).to eq(user.id)
+ end
end
- it 'does not create new labels' do
- expect { subject }.not_to change { Label.count }.from(3)
+ context 'when reporter maps to a GitLab user who is not a project member' do
+ let!(:user) { create(:user, email: 'reporter@example.com') }
+
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(project.creator.id)
+ end
+ end
+
+ context 'when reporter does not map to a GitLab user' do
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(project.creator.id)
+ end
+ end
+
+ context 'when reporter field is empty' do
+ let(:reporter) { nil }
+
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(project.creator.id)
+ end
+ end
+
+ context 'when reporter field is missing email address' do
+ let(:reporter) { double(name: 'Reporter', emailAddress: nil) }
+
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(project.creator.id)
+ end
+ end
+ end
+
+ context 'assignee' do
+ context 'when assignee maps to a GitLab user who is a project member' do
+ let!(:user) { create(:user, email: 'assignee@example.com') }
+
+ it 'sets the issue assignees to the mapped user' do
+ project.add_developer(user)
+
+ expect(subject[:assignee_ids]).to eq([user.id])
+ end
+ end
+
+ context 'when assignee maps to a GitLab user who is not a project member' do
+ let!(:user) { create(:user, email: 'assignee@example.com') }
+
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
+ end
+
+ context 'when assignee does not map to a GitLab user' do
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
+ end
+
+ context 'when assginee field is empty' do
+ let(:assignee) { nil }
+
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
+ end
+
+ context 'when assginee field is missing email address' do
+ let(:assignee) { double(name: 'Assignee', emailAddress: nil) }
+
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a46778bb6c3..da157eec39f 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -7,6 +7,8 @@ describe Gitlab::UsageData, :aggregate_failures do
before do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+
+ stub_object_store_settings
end
shared_examples "usage data execution" do
@@ -82,6 +84,16 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:clusters_management_project]).to eq(1)
end
+ it 'gathers object store usage correctly' do
+ expect(subject[:object_store]).to eq(
+ { artifacts: { enabled: true, object_store: { enabled: true, direct_upload: true, background_upload: false, provider: "AWS" } },
+ external_diffs: { enabled: false },
+ lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
+ uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
+ packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }
+ )
+ end
+
it 'works when queries time out' do
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
@@ -223,6 +235,66 @@ describe Gitlab::UsageData, :aggregate_failures do
end
end
+ describe '#object_store_config' do
+ let(:component) { 'lfs' }
+
+ subject { described_class.object_store_config(component) }
+
+ context 'when object_store is not configured' do
+ it 'returns component enable status only' do
+ allow(Settings).to receive(:[]).with(component).and_return({ 'enabled' => false })
+
+ expect(subject).to eq({ enabled: false })
+ end
+ end
+
+ context 'when object_store is configured' do
+ it 'returns filtered object store config' do
+ allow(Settings).to receive(:[]).with(component)
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => true,
+ 'remote_directory' => component,
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } })
+
+ expect(subject).to eq(
+ { enabled: true, object_store: { enabled: true, direct_upload: true, background_upload: false, provider: "AWS" } })
+ end
+ end
+
+ context 'when retrieve component setting meets exception' do
+ it 'returns -1 for component enable status' do
+ allow(Settings).to receive(:[]).with(component).and_raise(StandardError)
+
+ expect(subject).to eq({ enabled: -1 })
+ end
+ end
+ end
+
+ describe '#object_store_usage_data' do
+ subject { described_class.object_store_usage_data }
+
+ it 'fetches object store config of five components' do
+ %w(artifacts external_diffs lfs uploads packages).each do |component|
+ expect(described_class).to receive(:object_store_config).with(component).and_return("#{component}_object_store_config")
+ end
+
+ expect(subject).to eq(
+ object_store: {
+ artifacts: 'artifacts_object_store_config',
+ external_diffs: 'external_diffs_object_store_config',
+ lfs: 'lfs_object_store_config',
+ uploads: 'uploads_object_store_config',
+ packages: 'packages_object_store_config'
+ })
+ end
+ end
+
describe '#cycle_analytics_usage_data' do
subject { described_class.cycle_analytics_usage_data }
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
new file mode 100644
index 00000000000..b879965a261
--- /dev/null
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::InstanceVariable do
+ subject { build(:ci_instance_variable) }
+
+ it_behaves_like "CI variable"
+
+ it { is_expected.to include_module(Ci::Maskable) }
+ it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) }
+
+ describe '.unprotected' do
+ subject { described_class.unprotected }
+
+ context 'when variable is protected' do
+ before do
+ create(:ci_instance_variable, :protected)
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when variable is not protected' do
+ let(:variable) { create(:ci_instance_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 1f1e686fb21..e25fd310fbd 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -153,5 +153,59 @@ module UsageDataHelpers
projects_with_expiration_policy_enabled_with_older_than_set_to_14d
projects_with_expiration_policy_enabled_with_older_than_set_to_30d
projects_with_expiration_policy_enabled_with_older_than_set_to_90d
+ object_store
).freeze
+
+ def stub_object_store_settings
+ allow(Settings).to receive(:[]).with('artifacts')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => true,
+ 'remote_directory' => 'artifacts',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+
+ allow(Settings).to receive(:[]).with('external_diffs').and_return({ 'enabled' => false })
+
+ allow(Settings).to receive(:[]).with('lfs')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'lfs-objects',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+ allow(Settings).to receive(:[]).with('uploads')
+ .and_return(
+ { 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'uploads',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+ allow(Settings).to receive(:[]).with('packages')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'packages',
+ 'direct_upload' => false,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => true,
+ 'proxy_download' => false } }
+ )
+ end
end
diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
index 80629cb875e..36af65a3a06 100644
--- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
describe Gitlab::JiraImport::ImportIssueWorker do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
+ let_it_be(:jira_issue_label_1) { create(:label, project: project) }
+ let_it_be(:jira_issue_label_2) { create(:label, project: project) }
let(:some_key) { 'some-key' }
describe 'modules' do
@@ -17,7 +19,10 @@ describe Gitlab::JiraImport::ImportIssueWorker do
subject { described_class.new }
describe '#perform', :clean_gitlab_redis_cache do
- let(:issue_attrs) { build(:issue, project_id: project.id).as_json.compact }
+ let(:issue_attrs) do
+ build(:issue, project_id: project.id, title: 'jira issue')
+ .as_json.merge('label_ids' => [jira_issue_label_1.id, jira_issue_label_2.id]).compact
+ end
context 'when any exception raised while inserting to DB' do
before do
@@ -47,14 +52,22 @@ describe Gitlab::JiraImport::ImportIssueWorker do
context 'when import label exists' do
before do
Gitlab::JiraImport.cache_import_label_id(project.id, label.id)
- end
- it 'does not record import failure' do
subject.perform(project.id, 123, issue_attrs, some_key)
+ end
+ it 'does not record import failure' do
expect(label.issues.count).to eq(1)
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0)
end
+
+ it 'creates an issue with the correct attributes' do
+ issue = Issue.last
+
+ expect(issue.title).to eq('jira issue')
+ expect(issue.project).to eq(project)
+ expect(issue.labels).to match_array([label, jira_issue_label_1, jira_issue_label_2])
+ end
end
end
end