diff options
96 files changed, 1372 insertions, 307 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 06c3deda888..873876ecc94 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -328,13 +328,6 @@ RSpec/LeakyConstantDeclaration: Exclude: - 'spec/**/*.rb' - 'qa/spec/**/*.rb' - - 'ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb' - - 'ee/spec/lib/gitlab/geo/log_helpers_spec.rb' - - 'ee/spec/lib/gitlab/geo/replicator_spec.rb' - - 'ee/spec/mailers/emails/service_desk_spec.rb' - - 'ee/spec/migrations/remove_creations_in_gitlab_subscription_histories_spec.rb' - - 'ee/spec/migrations/set_resolved_state_on_vulnerabilities_spec.rb' - - 'ee/spec/models/repository_spec.rb' - 'ee/spec/presenters/security/vulnerable_project_presenter_spec.rb' - 'ee/spec/serializers/vulnerable_project_entity_spec.rb' - 'ee/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f6aa0e77005..bd9803aa943 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -491,13 +491,6 @@ Style/MultilineIfModifier: - 'app/services/ci/process_pipeline_service.rb' - 'lib/api/commit_statuses.rb' -# Offense count: 34 -# Cop supports --auto-correct. -# Configuration parameters: Whitelist. -# Whitelist: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with -Style/NestedParenthesizedCalls: - Enabled: false - # Offense count: 25 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength. @@ -493,3 +493,6 @@ gem 'erubi', '~> 1.9.0' # Monkey-patched in `config/initializers/mail_encoding_patch.rb` # See https://gitlab.com/gitlab-org/gitlab/issues/197386 gem 'mail', '= 2.7.1' + +# File encryption +gem 'lockbox', '~> 0.3.3' diff --git a/Gemfile.lock b/Gemfile.lock index 277eabf180d..ecd524d3afa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -605,6 +605,7 @@ GEM rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) locale (2.1.2) + lockbox (0.3.3) lograge (0.10.0) actionpack (>= 4) activesupport (>= 4) @@ -1280,6 +1281,7 @@ DEPENDENCIES license_finder (~> 5.4) licensee (~> 8.9) liquid (~> 4.0) + lockbox (~> 0.3.3) lograge (~> 0.5) loofah (~> 2.2) lru_redux diff --git a/app/assets/javascripts/monitoring/components/charts/bar.vue b/app/assets/javascripts/monitoring/components/charts/bar.vue new file mode 100644 index 00000000000..01fd8940dad --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/bar.vue @@ -0,0 +1,97 @@ +<script> +import { GlResizeObserverDirective } from '@gitlab/ui'; +import { GlBarChart } from '@gitlab/ui/dist/charts'; +import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; +import { chartHeight } from '../../constants'; +import { barChartsDataParser, graphDataValidatorForValues } from '../../utils'; + +export default { + components: { + GlBarChart, + }, + directives: { + GlResizeObserverDirective, + }, + props: { + graphData: { + type: Object, + required: true, + validator: graphDataValidatorForValues.bind(null, false), + }, + }, + data() { + return { + width: 0, + height: chartHeight, + svgs: {}, + }; + }, + computed: { + chartData() { + return barChartsDataParser(this.graphData.metrics); + }, + chartOptions() { + return { + dataZoom: [this.dataZoomConfig], + }; + }, + xAxisTitle() { + const { xLabel = '' } = this.graphData; + return xLabel; + }, + yAxisTitle() { + const { y_label = '' } = this.graphData; + return y_label; // eslint-disable-line babel/camelcase + }, + xAxisType() { + const { x_type = 'value' } = this.graphData; + return x_type; // eslint-disable-line babel/camelcase + }, + dataZoomConfig() { + const handleIcon = this.svgs['scroll-handle']; + + return handleIcon ? { handleIcon } : {}; + }, + }, + created() { + this.setSvg('scroll-handle'); + }, + methods: { + formatLegendLabel(query) { + return `${query.label}`; + }, + onResize() { + if (!this.$refs.barChart) return; + const { width } = this.$refs.barChart.$el.getBoundingClientRect(); + this.width = width; + }, + setSvg(name) { + getSvgIconPathContent(name) + .then(path => { + if (path) { + this.$set(this.svgs, name, `path://${path}`); + } + }) + .catch(e => { + // eslint-disable-next-line no-console, @gitlab/require-i18n-strings + console.error('SVG could not be rendered correctly: ', e); + }); + }, + }, +}; +</script> +<template> + <div v-gl-resize-observer-directive="onResize"> + <gl-bar-chart + ref="barChart" + v-bind="$attrs" + :data="chartData" + :option="chartOptions" + :width="width" + :height="height" + :x-axis-title="xAxisTitle" + :y-axis-title="yAxisTitle" + :x-axis-type="xAxisType" + /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 79f2c8ad41f..da305c7dda3 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -18,6 +18,7 @@ import MonitorAnomalyChart from './charts/anomaly.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorHeatmapChart from './charts/heatmap.vue'; import MonitorColumnChart from './charts/column.vue'; +import MonitorBarChart from './charts/bar.vue'; import MonitorStackedColumnChart from './charts/stacked_column.vue'; import MonitorEmptyChart from './charts/empty_chart.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; @@ -31,6 +32,7 @@ export default { components: { MonitorSingleStatChart, MonitorColumnChart, + MonitorBarChart, MonitorHeatmapChart, MonitorStackedColumnChart, MonitorEmptyChart, @@ -259,6 +261,10 @@ export default { v-else-if="isPanelType('heatmap') && graphDataHasMetrics" :graph-data="graphData" /> + <monitor-bar-chart + v-else-if="isPanelType('bar') && graphDataHasMetrics" + :graph-data="graphData" + /> <monitor-column-chart v-else-if="isPanelType('column') && graphDataHasMetrics" :graph-data="graphData" diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index cc7f5af2259..4fd6903d566 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -46,7 +46,6 @@ export const metricStates = { }; export const sidebarAnimationDuration = 300; // milliseconds. - export const chartHeight = 300; export const graphTypes = { diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 066d0bf7676..48ed2259a51 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -73,14 +73,21 @@ const mapToMetricsViewModel = (metrics, defaultLabel) => })); /** - * Maps an axis view model + * Maps X-axis view model + * + * @param {Object} axis + */ +const mapXAxisToViewModel = ({ name = '' }) => ({ name }); + +/** + * Maps Y-axis view model * * Defaults to a 2 digit precision and `number` format. It only allows * formats in the SUPPORTED_FORMATS array. * * @param {Object} axis */ -const mapToAxisViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, precision = 2 }) => { +const mapYAxisToViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, precision = 2 }) => { return { name, format: SUPPORTED_FORMATS[format] || SUPPORTED_FORMATS.number, @@ -94,15 +101,30 @@ const mapToAxisViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, prec * @param {Object} panel - Metrics panel * @returns {Object} */ -const mapToPanelViewModel = ({ title = '', type, y_label, y_axis = {}, metrics = [] }) => { +const mapPanelToViewModel = ({ + title = '', + type, + x_axis = {}, + x_label, + y_label, + y_axis = {}, + metrics = [], +}) => { + // Both `x_axis.name` and `x_label` are supported for now + // https://gitlab.com/gitlab-org/gitlab/issues/210521 + const xAxis = mapXAxisToViewModel({ name: x_label, ...x_axis }); // eslint-disable-line babel/camelcase + // Both `y_axis.name` and `y_label` are supported for now // https://gitlab.com/gitlab-org/gitlab/issues/208385 - const yAxis = mapToAxisViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase + const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase + return { title, type, + xLabel: xAxis.name, y_label: yAxis.name, // Changing y_label to yLabel is pending https://gitlab.com/gitlab-org/gitlab/issues/207198 yAxis, + xAxis, metrics: mapToMetricsViewModel(metrics, yAxis.name), }; }; @@ -117,7 +139,7 @@ const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => { return { key: `${slugify(group || 'default')}-${i}`, group, - panels: panels.map(mapToPanelViewModel), + panels: panels.map(mapPanelToViewModel), }; }; diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 6a46c7e67e4..7c6cd19eb7b 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -132,4 +132,63 @@ export const timeRangeToUrl = (timeRange, url = window.location.href) => { return mergeUrlParams(params, toUrl); }; +/** + * Get the metric value from first data point. + * Currently only used for bar charts + * + * @param {Array} values data points + * @returns {Number} + */ +const metricValueMapper = values => values[0]?.[1]; + +/** + * Get the metric name from metric object + * Currently only used for bar charts + * e.g. { handler: '/query' } + * { method: 'get' } + * + * @param {Object} metric metric object + * @returns {String} + */ +const metricNameMapper = metric => Object.values(metric)?.[0]; + +/** + * Parse metric object to extract metric value and name in + * [<metric-value>, <metric-name>] format. + * Currently only used for bar charts + * + * @param {Object} param0 metric object + * @returns {Array} + */ +const resultMapper = ({ metric, values = [] }) => [ + metricValueMapper(values), + metricNameMapper(metric), +]; + +/** + * Bar charts graph data parser to massage data from + * backend to a format acceptable by bar charts component + * in GitLab UI + * + * e.g. + * { + * SLO: [ + * [98, 'api'], + * [99, 'web'], + * [99, 'database'] + * ] + * } + * + * @param {Array} data series information + * @returns {Object} + */ +export const barChartsDataParser = (data = []) => + data?.reduce( + (acc, { result = [], label }) => ({ + ...acc, + [label]: result.map(resultMapper), + }), + {}, + ); + export default {}; diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss index 30ef047bf04..5872c90a25e 100644 --- a/app/assets/stylesheets/pages/prometheus.scss +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -93,6 +93,11 @@ .alert-current-setting { max-width: 240px; + + .badge.badge-danger { + color: $red-500; + background-color: $red-100; + } } .prometheus-graph-cursor { diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 26d73007e65..1f1ff75359d 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -30,8 +30,7 @@ module AuthHelper def qa_class_for_provider(provider) { - saml: 'qa-saml-login-button', - github: 'qa-github-login-button' + saml: 'qa-saml-login-button' }[provider.to_sym] end diff --git a/app/models/concerns/services/data_fields.rb b/app/models/concerns/services/data_fields.rb new file mode 100644 index 00000000000..10963e4e7d8 --- /dev/null +++ b/app/models/concerns/services/data_fields.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Services + module DataFields + extend ActiveSupport::Concern + + included do + belongs_to :service + + delegate :activated?, to: :service, allow_nil: true + + validates :service, presence: true + end + + class_methods do + def encryption_options + { + key: Settings.attr_encrypted_db_key_base_32, + encode: true, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm' + } + end + end + end +end diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb index cf406a784ce..12ebf260e08 100644 --- a/app/models/project_services/data_fields.rb +++ b/app/models/project_services/data_fields.rb @@ -44,6 +44,7 @@ module DataFields included do has_one :issue_tracker_data, autosave: true has_one :jira_tracker_data, autosave: true + has_one :open_project_tracker_data, autosave: true def data_fields raise NotImplementedError diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb index b1d67657fe6..414f2c1da4d 100644 --- a/app/models/project_services/issue_tracker_data.rb +++ b/app/models/project_services/issue_tracker_data.rb @@ -1,20 +1,7 @@ # frozen_string_literal: true class IssueTrackerData < ApplicationRecord - belongs_to :service - - delegate :activated?, to: :service, allow_nil: true - - validates :service, presence: true - - def self.encryption_options - { - key: Settings.attr_encrypted_db_key_base_32, - encode: true, - mode: :per_attribute_iv, - algorithm: 'aes-256-gcm' - } - end + include Services::DataFields attr_encrypted :project_url, encryption_options attr_encrypted :issues_url, encryption_options diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb index e4e0f64150a..f24ba8877d2 100644 --- a/app/models/project_services/jira_tracker_data.rb +++ b/app/models/project_services/jira_tracker_data.rb @@ -1,20 +1,7 @@ # frozen_string_literal: true class JiraTrackerData < ApplicationRecord - belongs_to :service - - delegate :activated?, to: :service, allow_nil: true - - validates :service, presence: true - - def self.encryption_options - { - key: Settings.attr_encrypted_db_key_base_32, - encode: true, - mode: :per_attribute_iv, - algorithm: 'aes-256-gcm' - } - end + include Services::DataFields attr_encrypted :url, encryption_options attr_encrypted :api_url, encryption_options diff --git a/app/models/project_services/open_project_service.rb b/app/models/project_services/open_project_service.rb new file mode 100644 index 00000000000..a24fbc1611d --- /dev/null +++ b/app/models/project_services/open_project_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class OpenProjectService < IssueTrackerService + validates :url, public_url: true, presence: true, if: :activated? + validates :api_url, public_url: true, allow_blank: true, if: :activated? + validates :token, presence: true, if: :activated? + validates :project_identifier_code, presence: true, if: :activated? + + data_field :url, :api_url, :token, :closed_status_id, :project_identifier_code + + def data_fields + open_project_tracker_data || self.build_open_project_tracker_data + end + + def self.to_param + 'open_project' + end +end diff --git a/app/models/project_services/open_project_tracker_data.rb b/app/models/project_services/open_project_tracker_data.rb new file mode 100644 index 00000000000..20de60e40c1 --- /dev/null +++ b/app/models/project_services/open_project_tracker_data.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class OpenProjectTrackerData < ApplicationRecord + include Services::DataFields + + # When the Open Project is fresh installed, the default closed status id is "13" based on current version: v8. + DEFAULT_CLOSED_STATUS_ID = "13" + + attr_encrypted :url, encryption_options + attr_encrypted :api_url, encryption_options + attr_encrypted :token, encryption_options + + def closed_status_id + super || DEFAULT_CLOSED_STATUS_ID + end +end diff --git a/app/models/terraform.rb b/app/models/terraform.rb new file mode 100644 index 00000000000..3d974ffe051 --- /dev/null +++ b/app/models/terraform.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Terraform + def self.table_name_prefix + 'terraform_' + end +end diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb new file mode 100644 index 00000000000..8ca4ee9239a --- /dev/null +++ b/app/models/terraform/state.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Terraform + class State < ApplicationRecord + belongs_to :project + + validates :project_id, presence: true + + after_save :update_file_store, if: :saved_change_to_file? + + mount_uploader :file, StateUploader + + def update_file_store + # The file.object_store is set during `uploader.store!` + # which happens after object is inserted/updated + self.update_column(:file_store, file.object_store) + end + + def file_store + super || StateUploader.default_store + end + end +end diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb index 6da30f8af16..bc681397039 100644 --- a/app/services/merge_requests/after_create_service.rb +++ b/app/services/merge_requests/after_create_service.rb @@ -6,11 +6,8 @@ module MergeRequests event_service.open_mr(merge_request, current_user) notification_service.new_merge_request(merge_request, current_user) - # https://gitlab.com/gitlab-org/gitlab/issues/208813 - if ::Feature.enabled?(:create_merge_request_pipelines_in_sidekiq, project) - create_pipeline_for(merge_request, current_user) - merge_request.update_head_pipeline - end + create_pipeline_for(merge_request, current_user) + merge_request.update_head_pipeline merge_request.diffs(include_stats: false).write_cache merge_request.create_cross_references!(current_user) diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index d957a6425b2..1cdfba41432 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -21,12 +21,6 @@ module MergeRequests todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) - # https://gitlab.com/gitlab-org/gitlab/issues/208813 - unless ::Feature.enabled?(:create_merge_request_pipelines_in_sidekiq, project) - create_pipeline_for(issuable, current_user) - issuable.update_head_pipeline - end - Gitlab::UsageDataCounters::MergeRequestCounter.count(:create) link_lfs_objects(issuable) diff --git a/app/uploaders/terraform/state_uploader.rb b/app/uploaders/terraform/state_uploader.rb new file mode 100644 index 00000000000..9c5ae8a8bdc --- /dev/null +++ b/app/uploaders/terraform/state_uploader.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Terraform + class StateUploader < GitlabUploader + include ObjectStorage::Concern + + storage_options Gitlab.config.terraform_state + + delegate :project_id, to: :model + + # Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks) + encrypt(key: :key) + + def filename + "#{model.id}.tfstate" + end + + def store_dir + project_id.to_s + end + + def key + OpenSSL::HMAC.digest('SHA256', Gitlab::Application.secrets.db_key_base, project_id.to_s) + end + + class << self + def direct_upload_enabled? + false + end + + def background_upload_enabled? + false + end + + def proxy_download_enabled? + true + end + + def default_store + object_store_enabled? ? ObjectStorage::Store::REMOTE : ObjectStorage::Store::LOCAL + end + end + end +end diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 849f92fdc95..c47a8891fd5 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -5,4 +5,4 @@ -# per_page: number of items to fetch per page -# remote: data-remote %li.page-item.disabled.d-none.d-md-block - = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link' + = link_to raw(t('views.pagination.truncate')), '#', class: 'page-link' diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 1a1da6b3801..e20573ed3a7 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -129,7 +129,7 @@ = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar - - if Feature.enabled?(:save_issuable_health_status, @project.group) && issuable_sidebar[:type] == "issue" + - if issuable_sidebar.dig(:features_available, :health_status) .js-sidebar-status-entry-point - if issuable_sidebar.has_key?(:confidential) diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml index a9af732bbb5..d552c1a723b 100644 --- a/app/views/shared/snippets/show.js.haml +++ b/app/views/shared/snippets/show.js.haml @@ -1,2 +1,2 @@ -document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}'); -document.write('#{escape_javascript(render 'shared/snippets/embed')}'); +document.write('#{escape_javascript(stylesheet_link_tag("#{stylesheet_url 'snippets'}"))}'); +document.write('#{escape_javascript(render('shared/snippets/embed'))}'); diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 91ab0d69ad1..73bc050d7be 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -133,7 +133,7 @@ class IrkerWorker # rubocop:disable Scalability/IdempotentWorker commit = commit_from_id project, hook_attrs['id'] sha = colorize_sha Commit.truncate_sha(hook_attrs['id']) author = hook_attrs['author']['name'] - files = colorize_nb_files(files_count commit) + files = colorize_nb_files(files_count(commit)) title = commit.title sendtoirker "#{repo_name}/#{branch} #{sha} #{author} (#{files}): #{title}" diff --git a/changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml b/changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml new file mode 100644 index 00000000000..31d4c0d1587 --- /dev/null +++ b/changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml @@ -0,0 +1,5 @@ +--- +title: Create model to store Terraform state files +merge_request: 26619 +author: +type: added diff --git a/changelogs/unreleased/208884-optimize-ci_builds-counters-in-non-mau.yml b/changelogs/unreleased/208884-optimize-ci_builds-counters-in-non-mau.yml new file mode 100644 index 00000000000..71a1f122816 --- /dev/null +++ b/changelogs/unreleased/208884-optimize-ci_builds-counters-in-non-mau.yml @@ -0,0 +1,5 @@ +--- +title: Optimize ci builds non distinct counters in usage data +merge_request: 28027 +author: +type: performance diff --git a/changelogs/unreleased/add-bar-charts-to-monitoring-dashboard.yml b/changelogs/unreleased/add-bar-charts-to-monitoring-dashboard.yml new file mode 100644 index 00000000000..a33d6f82d42 --- /dev/null +++ b/changelogs/unreleased/add-bar-charts-to-monitoring-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add bar chart support to monitoring dashboard +merge_request: 27155 +author: +type: added diff --git a/changelogs/unreleased/configure-additional-rails-hosts-with-env-variable.yml b/changelogs/unreleased/configure-additional-rails-hosts-with-env-variable.yml new file mode 100644 index 00000000000..23f21f772f7 --- /dev/null +++ b/changelogs/unreleased/configure-additional-rails-hosts-with-env-variable.yml @@ -0,0 +1,5 @@ +--- +title: Add possibility to conigure additional rails hosts with env variable +merge_request: 28133 +author: +type: other diff --git a/changelogs/unreleased/remove-pipelines-in-sidekiq-flag.yml b/changelogs/unreleased/remove-pipelines-in-sidekiq-flag.yml new file mode 100644 index 00000000000..099d9cce435 --- /dev/null +++ b/changelogs/unreleased/remove-pipelines-in-sidekiq-flag.yml @@ -0,0 +1,5 @@ +--- +title: Create merge request pipelines in background jobs +merge_request: 28024 +author: +type: performance diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index eb41e4ac423..b66389b1a6f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -320,6 +320,24 @@ production: &base # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4. # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' + ## Terraform state + terraform_state: + enabled: true + # The location where Terraform state files are stored (default: shared/terraform_state). + # storage_path: shared/terraform_state + object_store: + enabled: false + remote_directory: terraform_state # The bucket name + connection: + provider: AWS + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: us-east-1 + # host: 'localhost' # default: s3.amazonaws.com + # endpoint: 'http://127.0.0.1:9000' # default: nil + # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4. + # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' + ## GitLab Pages pages: enabled: false @@ -1193,6 +1211,19 @@ test: aws_access_key_id: AWS_ACCESS_KEY_ID aws_secret_access_key: AWS_SECRET_ACCESS_KEY region: us-east-1 + + terraform_state: + enabled: true + storage_path: tmp/tests/terraform_state + object_store: + enabled: false + remote_directory: terraform_state + connection: + provider: AWS # Only AWS supported at the moment + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: us-east-1 + gitlab: host: localhost port: 80 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 79bfcfd79e1..f31762d9ac6 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -370,6 +370,14 @@ Gitlab.ee do end # +# Terraform state +# +Settings['terraform_state'] ||= Settingslogic.new({}) +Settings.terraform_state['enabled'] = true if Settings.terraform_state['enabled'].nil? +Settings.terraform_state['storage_path'] = Settings.absolute(Settings.terraform_state['storage_path'] || File.join(Settings.shared['path'], "terraform_state")) +Settings.terraform_state['object_store'] = ObjectStoreSettings.parse(Settings.terraform_state['object_store']) + +# # Mattermost # Settings['mattermost'] ||= Settingslogic.new({}) diff --git a/config/initializers/rails_host_authorization.rb b/config/initializers/rails_host_authorization.rb index 3be2de47812..6cca39ea95b 100644 --- a/config/initializers/rails_host_authorization.rb +++ b/config/initializers/rails_host_authorization.rb @@ -4,4 +4,9 @@ if Rails.env.development? Rails.application.config.hosts += [Gitlab.config.gitlab.host, 'unix'] + + if ENV['RAILS_HOSTS'] + additional_hosts = ENV['RAILS_HOSTS'].split(',').select(&:presence) + Rails.application.config.hosts += additional_hosts + end end diff --git a/db/migrate/20200305200641_create_terraform_states.rb b/db/migrate/20200305200641_create_terraform_states.rb new file mode 100644 index 00000000000..3e137369e33 --- /dev/null +++ b/db/migrate/20200305200641_create_terraform_states.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateTerraformStates < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + create_table :terraform_states do |t| + t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false + t.timestamps_with_timezone null: false + t.integer :file_store, limit: 2 + t.string :file, limit: 255 + end + end +end diff --git a/db/migrate/20200325160952_add_index_on_name_type_eq_ci_build_to_ci_builds.rb b/db/migrate/20200325160952_add_index_on_name_type_eq_ci_build_to_ci_builds.rb new file mode 100644 index 00000000000..7e15c3ad11d --- /dev/null +++ b/db/migrate/20200325160952_add_index_on_name_type_eq_ci_build_to_ci_builds.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddIndexOnNameTypeEqCiBuildToCiBuilds < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + INDEX_NAME = 'index_ci_builds_on_name_and_security_type_eq_ci_build' + + def up + add_concurrent_index :ci_builds, [:name, :id], + name: INDEX_NAME, + where: "((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text)" + end + + def down + remove_concurrent_index_by_name :ci_builds, INDEX_NAME + end +end diff --git a/db/structure.sql b/db/structure.sql index bb291590abc..a972ef7085a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5976,6 +5976,24 @@ CREATE SEQUENCE public.term_agreements_id_seq ALTER SEQUENCE public.term_agreements_id_seq OWNED BY public.term_agreements.id; +CREATE TABLE public.terraform_states ( + id bigint NOT NULL, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + file_store smallint, + file character varying(255) +); + +CREATE SEQUENCE public.terraform_states_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.terraform_states_id_seq OWNED BY public.terraform_states.id; + CREATE TABLE public.timelogs ( id integer NOT NULL, time_spent integer NOT NULL, @@ -7329,6 +7347,8 @@ ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id ALTER TABLE ONLY public.term_agreements ALTER COLUMN id SET DEFAULT nextval('public.term_agreements_id_seq'::regclass); +ALTER TABLE ONLY public.terraform_states ALTER COLUMN id SET DEFAULT nextval('public.terraform_states_id_seq'::regclass); + ALTER TABLE ONLY public.timelogs ALTER COLUMN id SET DEFAULT nextval('public.timelogs_id_seq'::regclass); ALTER TABLE ONLY public.todos ALTER COLUMN id SET DEFAULT nextval('public.todos_id_seq'::regclass); @@ -8230,6 +8250,9 @@ ALTER TABLE ONLY public.tags ALTER TABLE ONLY public.term_agreements ADD CONSTRAINT term_agreements_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.terraform_states + ADD CONSTRAINT terraform_states_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.timelogs ADD CONSTRAINT timelogs_pkey PRIMARY KEY (id); @@ -8604,6 +8627,8 @@ CREATE INDEX index_ci_builds_on_commit_id_and_type_and_name_and_ref ON public.ci CREATE INDEX index_ci_builds_on_commit_id_and_type_and_ref ON public.ci_builds USING btree (commit_id, type, ref); +CREATE INDEX index_ci_builds_on_name_and_security_type_eq_ci_build ON public.ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text)); + CREATE INDEX index_ci_builds_on_name_for_security_reports_values ON public.ci_builds USING btree (name) WHERE ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text])); CREATE INDEX index_ci_builds_on_project_id_and_id ON public.ci_builds USING btree (project_id, id); @@ -9972,6 +9997,8 @@ CREATE INDEX index_term_agreements_on_term_id ON public.term_agreements USING bt CREATE INDEX index_term_agreements_on_user_id ON public.term_agreements USING btree (user_id); +CREATE INDEX index_terraform_states_on_project_id ON public.terraform_states USING btree (project_id); + CREATE INDEX index_timelogs_on_issue_id ON public.timelogs USING btree (issue_id); CREATE INDEX index_timelogs_on_merge_request_id ON public.timelogs USING btree (merge_request_id); @@ -11277,6 +11304,9 @@ ALTER TABLE ONLY public.pages_domain_acme_orders ALTER TABLE ONLY public.ci_subscriptions_projects ADD CONSTRAINT fk_rails_7871f9a97b FOREIGN KEY (upstream_project_id) REFERENCES public.projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.terraform_states + ADD CONSTRAINT fk_rails_78f54ca485 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.software_license_policies ADD CONSTRAINT fk_rails_7a7a2a92de FOREIGN KEY (software_license_id) REFERENCES public.software_licenses(id) ON DELETE CASCADE; @@ -12749,6 +12779,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200304211738 20200305121159 20200305151736 +20200305200641 20200306095654 20200306160521 20200306170211 @@ -12807,5 +12838,6 @@ COPY "schema_migrations" (version) FROM STDIN; 20200323122201 20200323134519 20200324115359 +20200325160952 \. diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md new file mode 100644 index 00000000000..c684178f13e --- /dev/null +++ b/doc/administration/terraform_state.md @@ -0,0 +1,135 @@ +# Terraform state administration (alpha) + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2673) in GitLab 12.10. + +GitLab can be used as a backend for [Terraform](../user/infrastructure/index.md) state +files. The files are encrypted before being stored. This feature is enabled by default. + +The storage location of these files defaults to: + +- `/var/opt/gitlab/gitlab-rails/shared/terraform_state` for Omnibus GitLab installations. +- `/home/git/gitlab/shared/terraform_state` for source installations. + +These locations can be configured using the options described below. + +## Using local storage + +NOTE: **Note:** +This is the default configuration + +To change the location where Terraform state files are stored locally, follow the steps +below. + +**In Omnibus installations:** + +1. To change the storage path for example to `/mnt/storage/terraform_state`, edit + `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['terraform_state_enabled'] = true + gitlab_rails['terraform_state_storage_path'] = "/mnt/storage/terraform_state" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**In installations from source:** + +1. To change the storage path for example to `/mnt/storage/terraform_state`, edit + `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: + + ```yaml + terraform_state: + enabled: true + storage_path: /mnt/storage/terraform_state + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Using object storage **(CORE ONLY)** + +Instead of storing Terraform state files on disk, we recommend the use of an object +store that is S3-compatible instead. This configuration relies on valid credentials to +be configured already. + +### Object storage settings + +The following settings are: + +- Nested under `terraform_state:` and then `object_store:` on source installations. +- Prefixed by `terraform_state_object_store_` on Omnibus GitLab installations. + +| Setting | Description | Default | +|---------|-------------|---------| +| `enabled` | Enable/disable object storage | `true` | +| `remote_directory` | The bucket name where Terraform state files will be stored | | +| `connection` | Various connection options described below | | + +### S3-compatible connection settings + +The connection settings match those provided by [Fog](https://github.com/fog), and are as follows: + +| Setting | Description | Default | +|---------|-------------|---------| +| `provider` | Always `AWS` for compatible hosts | `AWS` | +| `aws_access_key_id` | Credentials for AWS or compatible provider | | +| `aws_secret_access_key` | Credentials for AWS or compatible provider | | +| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 | +| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | `true` | +| `region` | AWS region | us-east-1 | +| `host` | S3-compatible host when not using AWS. For example, `localhost` or `storage.example.com` | `s3.amazonaws.com` | +| `endpoint` | Can be used when configuring an S3-compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) | +| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | `false` | +| `use_iam_profile` | For AWS S3, set to true to use an IAM profile instead of access keys | `false` | + +**In Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following lines; replacing with + the values you want: + + ```ruby + gitlab_rails['terraform_state_enabled'] = true + gitlab_rails['terraform_state_object_store_enabled'] = true + gitlab_rails['terraform_state_object_store_remote_directory'] = "terraform_state" + gitlab_rails['terraform_state_object_store_connection'] = { + 'provider' => 'AWS', + 'region' => 'eu-central-1', + 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID', + 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY' + } + ``` + + NOTE: **Note:** + If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs. + + ```ruby + gitlab_rails['terraform_state_object_store_connection'] = { + 'provider' => 'AWS', + 'region' => 'eu-central-1', + 'use_iam_profile' => true + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**In installations from source:** + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following + lines: + + ```yaml + terraform_state: + enabled: true + object_store: + enabled: true + remote_directory: "terraform_state" # The bucket name + connection: + provider: AWS # Only AWS supported at the moment + aws_access_key_id: AWS_ACESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: eu-central-1 + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" +[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 6c5de925fe9..f6a1cb79a1f 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6229,6 +6229,11 @@ type Project { ): Requirement """ + Number of requirements for the project by their state + """ + requirementStatesCount: RequirementStatesCount + + """ Find requirements. Available only when feature flag `requirements_management` is enabled. """ requirements( @@ -7030,6 +7035,21 @@ enum RequirementState { OPENED } +""" +Counts of requirements by their state. +""" +type RequirementStatesCount { + """ + Number of archived requirements + """ + archived: Int + + """ + Number of opened requirements + """ + opened: Int +} + type RootStorageStatistics { """ The CI artifacts size in bytes diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 14d4f798f6e..a8f6923927b 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -18630,6 +18630,20 @@ "deprecationReason": null }, { + "name": "requirementStatesCount", + "description": "Number of requirements for the project by their state", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "RequirementStatesCount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "requirements", "description": "Find requirements. Available only when feature flag `requirements_management` is enabled.", "args": [ @@ -21116,6 +21130,47 @@ }, { "kind": "OBJECT", + "name": "RequirementStatesCount", + "description": "Counts of requirements by their state.", + "fields": [ + { + "name": "archived", + "description": "Number of archived requirements", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "opened", + "description": "Number of opened requirements", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "RootStorageStatistics", "description": null, "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2aee7d484d0..fb13c674347 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -910,6 +910,7 @@ Information about pagination in a connection. | `repository` | Repository | Git repository of the project | | `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project | | `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. | +| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state | | `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project | | `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project | | `serviceDeskAddress` | String | E-mail address of the service desk. | @@ -1032,6 +1033,15 @@ Check permissions for the current user on a requirement | `readRequirement` | Boolean! | Indicates the user can perform `read_requirement` on this resource | | `updateRequirement` | Boolean! | Indicates the user can perform `update_requirement` on this resource | +## RequirementStatesCount + +Counts of requirements by their state. + +| Name | Type | Description | +| --- | ---- | ---------- | +| `archived` | Int | Number of archived requirements | +| `opened` | Int | Number of opened requirements | + ## RootStorageStatistics | Name | Type | Description | diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb index d4ba4d1181d..b272adf8166 100644 --- a/lib/gitlab/import_formatter.rb +++ b/lib/gitlab/import_formatter.rb @@ -14,5 +14,10 @@ module Gitlab author ||= "Anonymous" "*Created by: #{author}*\n\n" end + + def assignee_line(assignee) + assignee ||= "Anonymous" + "*Assigned to: #{assignee}*\n\n" + end end end diff --git a/lib/gitlab/jira_import/issue_serializer.rb b/lib/gitlab/jira_import/issue_serializer.rb index f00852a21a2..0be5d22065a 100644 --- a/lib/gitlab/jira_import/issue_serializer.rb +++ b/lib/gitlab/jira_import/issue_serializer.rb @@ -3,12 +3,50 @@ module Gitlab module JiraImport class IssueSerializer + attr_reader :jira_issue, :project, :params, :formatter + def initialize(project, jira_issue, params = {}) + @jira_issue = jira_issue + @project = project + @params = params + @formatter = Gitlab::ImportFormatter.new end def execute - # this is going to be implemented in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27201 - {} + { + iid: params[:iid], + project_id: project.id, + description: description, + title: title, + 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 + } + end + + private + + def title + "[#{jira_issue.key}] #{jira_issue.summary}" + end + + 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.join + end + + def map_status(jira_status_category) + case jira_status_category["key"].downcase + when 'done' + Issuable::STATE_ID_MAP[:closed] + else + Issuable::STATE_ID_MAP[:opened] + end end end end diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb index 77fc216738f..33111b46381 100644 --- a/lib/gitlab/usage_data_counters/base_counter.rb +++ b/lib/gitlab/usage_data_counters/base_counter.rb @@ -14,11 +14,11 @@ module Gitlab::UsageDataCounters end def count(event) - increment(redis_key event) + increment(redis_key(event)) end def read(event) - total_count(redis_key event) + total_count(redis_key(event)) end def totals diff --git a/lib/system_check/app/git_config_check.rb b/lib/system_check/app/git_config_check.rb index 4e8d607096c..d0b64b8bfeb 100644 --- a/lib/system_check/app/git_config_check.rb +++ b/lib/system_check/app/git_config_check.rb @@ -36,7 +36,7 @@ module SystemCheck sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{OPTIONS['core.autocrlf']}\"") ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) end end diff --git a/lib/system_check/app/gitlab_config_exists_check.rb b/lib/system_check/app/gitlab_config_exists_check.rb index 1cc5ead0d89..a05aab1a5d2 100644 --- a/lib/system_check/app/gitlab_config_exists_check.rb +++ b/lib/system_check/app/gitlab_config_exists_check.rb @@ -17,7 +17,7 @@ module SystemCheck 'Update config/gitlab.yml to match your setup' ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/lib/system_check/app/gitlab_config_up_to_date_check.rb b/lib/system_check/app/gitlab_config_up_to_date_check.rb index 58c7e3039c8..193d9b77e5b 100644 --- a/lib/system_check/app/gitlab_config_up_to_date_check.rb +++ b/lib/system_check/app/gitlab_config_up_to_date_check.rb @@ -23,7 +23,7 @@ module SystemCheck 'Update config/gitlab.yml to match your setup' ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/lib/system_check/app/init_script_exists_check.rb b/lib/system_check/app/init_script_exists_check.rb index d36dbe7d67d..7be92acdc37 100644 --- a/lib/system_check/app/init_script_exists_check.rb +++ b/lib/system_check/app/init_script_exists_check.rb @@ -20,7 +20,7 @@ module SystemCheck 'Install the init script' ) for_more_information( - see_installation_guide_section 'Install Init Script' + see_installation_guide_section('Install Init Script') ) fix_and_rerun end diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb index 7e2e168af8b..cf841d5e659 100644 --- a/lib/system_check/app/init_script_up_to_date_check.rb +++ b/lib/system_check/app/init_script_up_to_date_check.rb @@ -32,7 +32,7 @@ module SystemCheck 'Re-download the init script' ) for_more_information( - see_installation_guide_section 'Install Init Script' + see_installation_guide_section('Install Init Script') ) fix_and_rerun end diff --git a/lib/system_check/app/log_writable_check.rb b/lib/system_check/app/log_writable_check.rb index e26ad143eb8..2c108f0c18d 100644 --- a/lib/system_check/app/log_writable_check.rb +++ b/lib/system_check/app/log_writable_check.rb @@ -15,7 +15,7 @@ module SystemCheck "sudo chmod -R u+rwX #{log_path}" ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/lib/system_check/app/tmp_writable_check.rb b/lib/system_check/app/tmp_writable_check.rb index 6687df091d3..937a5e1345f 100644 --- a/lib/system_check/app/tmp_writable_check.rb +++ b/lib/system_check/app/tmp_writable_check.rb @@ -15,7 +15,7 @@ module SystemCheck "sudo chmod -R u+rwX #{tmp_path}" ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/lib/system_check/app/uploads_directory_exists_check.rb b/lib/system_check/app/uploads_directory_exists_check.rb index 940eff9d4cf..54dff63ab61 100644 --- a/lib/system_check/app/uploads_directory_exists_check.rb +++ b/lib/system_check/app/uploads_directory_exists_check.rb @@ -14,7 +14,7 @@ module SystemCheck "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads" ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/lib/system_check/app/uploads_path_permission_check.rb b/lib/system_check/app/uploads_path_permission_check.rb index 4a49f3bc2bb..2e1cc687c43 100644 --- a/lib/system_check/app/uploads_path_permission_check.rb +++ b/lib/system_check/app/uploads_path_permission_check.rb @@ -19,7 +19,7 @@ module SystemCheck "sudo chmod 700 #{uploads_fullpath}" ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/lib/system_check/app/uploads_path_tmp_permission_check.rb b/lib/system_check/app/uploads_path_tmp_permission_check.rb index ae374f4707c..567c7540777 100644 --- a/lib/system_check/app/uploads_path_tmp_permission_check.rb +++ b/lib/system_check/app/uploads_path_tmp_permission_check.rb @@ -23,7 +23,7 @@ module SystemCheck "sudo find #{uploads_fullpath} -type d -not -path #{uploads_fullpath} -exec chmod 0700 {} \\;" ) for_more_information( - see_installation_guide_section 'GitLab' + see_installation_guide_section('GitLab') ) fix_and_rerun end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7a718a39a4e..444fea14ff9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -15900,6 +15900,9 @@ msgstr "" msgid "PrometheusAlerts|%{count} alerts applied" msgstr "" +msgid "PrometheusAlerts|%{firingCount} firing" +msgstr "" + msgid "PrometheusAlerts|Add alert" msgstr "" @@ -15918,6 +15921,12 @@ msgstr "" msgid "PrometheusAlerts|Error saving alert" msgstr "" +msgid "PrometheusAlerts|Firing: %{alerts}" +msgstr "" + +msgid "PrometheusAlerts|Firing: %{alert}" +msgstr "" + msgid "PrometheusAlerts|Operator" msgstr "" @@ -134,7 +134,6 @@ module QA autoload :LDAPNoServer, 'qa/scenario/test/integration/ldap_no_server' autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls' autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml' - autoload :OAuth, 'qa/scenario/test/integration/oauth' autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage' @@ -483,17 +482,6 @@ module QA autoload :ConfigureJob, 'qa/vendor/jenkins/page/configure_job' end end - - module Github - module Page - autoload :Base, 'qa/vendor/github/page/base' - autoload :Login, 'qa/vendor/github/page/login' - end - end - - module OnePassword - autoload :CLI, 'qa/vendor/one_password/cli' - end end # Classes that provide support to other parts of the framework. diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index cb3421f93c2..72884e97185 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -35,7 +35,6 @@ module QA view 'app/helpers/auth_helper.rb' do element :saml_login_button - element :github_login_button end view 'app/views/layouts/devise.html.haml' do @@ -139,11 +138,6 @@ module QA click_element :standard_tab end - def sign_in_with_github - set_initial_password_if_present - click_element :github_login_button - end - def sign_in_with_saml set_initial_password_if_present click_element :saml_login_button diff --git a/qa/qa/scenario/test/integration/oauth.rb b/qa/qa/scenario/test/integration/oauth.rb deleted file mode 100644 index 912156fbc29..00000000000 --- a/qa/qa/scenario/test/integration/oauth.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module QA - module Scenario - module Test - module Integration - class OAuth < Test::Instance::All - tags :oauth - end - end - end - end -end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb deleted file mode 100644 index dea85f68be2..00000000000 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module QA - # This test is skipped instead of quarantine because continuously running - # this test may cause the user to hit GitHub's rate limits thus blocking the user. - # Related issue: https://gitlab.com/gitlab-org/gitlab/issues/196517 - context 'Manage', :orchestrated, :oauth, :skip do - describe 'OAuth login' do - it 'User logs in to GitLab with GitHub OAuth' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - - Page::Main::Login.perform(&:sign_in_with_github) - Vendor::Github::Page::Login.perform(&:login) - - expect(page).to have_content('Welcome to GitLab') - end - end - end -end diff --git a/qa/qa/vendor/github/page/base.rb b/qa/qa/vendor/github/page/base.rb deleted file mode 100644 index 3b96180afe9..00000000000 --- a/qa/qa/vendor/github/page/base.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module QA - module Vendor - module Github - module Page - class Base - include Capybara::DSL - include Scenario::Actable - end - end - end - end -end diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb deleted file mode 100644 index 4675e33b514..00000000000 --- a/qa/qa/vendor/github/page/login.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' -require 'benchmark' - -module QA - module Vendor - module Github - module Page - class Login < Page::Base - def login - fill_in 'login', with: QA::Runtime::Env.github_username - fill_in 'password', with: QA::Runtime::Env.github_password - click_on 'Sign in' - - Support::Retrier.retry_until(raise_on_failure: true, sleep_interval: 35) do - fresh_otp = nil - - time = Benchmark.realtime do - fresh_otp = OnePassword::CLI.instance.fresh_otp - end - - QA::Runtime::Logger.info("Returned fresh_otp: #{fresh_otp} in #{time} seconds") - - fill_in 'otp', with: fresh_otp - - click_on 'Verify' - - !has_text?('Two-factor authentication failed', wait: 1.0) - end - - click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa') - end - end - end - end - end -end diff --git a/qa/qa/vendor/one_password/cli.rb b/qa/qa/vendor/one_password/cli.rb deleted file mode 100644 index cf8b7f8a4f9..00000000000 --- a/qa/qa/vendor/one_password/cli.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'benchmark' - -module QA - module Vendor - module OnePassword - class CLI - include Singleton - - def initialize - @email = QA::Runtime::Env.gitlab_qa_1p_email - @password = QA::Runtime::Env.gitlab_qa_1p_password - @secret = QA::Runtime::Env.gitlab_qa_1p_secret - @github_uuid = QA::Runtime::Env.gitlab_qa_1p_github_uuid - end - - def fresh_otp - otps = [] - - # Fetches a fresh OTP and returns it only after op provides the same OTP twice - # An OTP is valid for 30 seconds so 70 attempts with 0.5 interval would ensure we complete 1 cycle - Support::Retrier.retry_until(max_attempts: 70, sleep_interval: 0.5) do - otps << fetch_otp - otps.size >= 3 && otps[-1] == otps[-2] && otps[-1] != otps[-3] - end - - otps.last - end - - private - - def fetch_otp - result = nil - - time = Benchmark.realtime do - result = `#{op_path} get totp #{@github_uuid} --session=#{session_token}`.to_i - end - - QA::Runtime::Logger.info("Fetched OTP: #{result} in: #{time} seconds") - - result - end - - # OP session tokens are valid for 30 minutes. We are caching the session token here and this is fine currently - # as we just have one test that is not expected to go over 30 minutes. - # But note that if we add more tests that use this class, we might need to add a mechanism to invalidate - # the cache after 30 minutes or if the session_token is rejected by op CLI. - def session_token - @session_token ||= `echo '#{@password}' | #{op_path} signin gitlab.1password.com #{@email} #{@secret} --output=raw --shorthand=gitlab_qa` - end - - def op_path - File.expand_path(File.join(%W[qa vendor one_password #{os} op])) - end - - def os - RUBY_PLATFORM.include?("darwin") ? "darwin" : "linux" - end - end - end - end -end diff --git a/qa/qa/vendor/one_password/darwin/op b/qa/qa/vendor/one_password/darwin/op Binary files differdeleted file mode 100755 index be7a3721b14..00000000000 --- a/qa/qa/vendor/one_password/darwin/op +++ /dev/null diff --git a/qa/qa/vendor/one_password/linux/op b/qa/qa/vendor/one_password/linux/op Binary files differdeleted file mode 100755 index 47e79d7c599..00000000000 --- a/qa/qa/vendor/one_password/linux/op +++ /dev/null diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 2d13889d26d..f6080bcad49 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -23,12 +23,12 @@ describe QA::Page::Base do it 'makes it possible to define page views' do expect(subject.views.size).to eq 2 - expect(subject.views).to all(be_an_instance_of QA::Page::View) + expect(subject.views).to all(be_an_instance_of(QA::Page::View)) end it 'populates views objects with data about elements' do expect(subject.elements.size).to eq 3 - expect(subject.elements).to all(be_an_instance_of QA::Page::Element) + expect(subject.elements).to all(be_an_instance_of(QA::Page::Element)) expect(subject.elements.map(&:name)) .to eq [:something, :something_else, :another_element] end diff --git a/qa/spec/scenario/test/integration/oauth_spec.rb b/qa/spec/scenario/test/integration/oauth_spec.rb deleted file mode 100644 index ab7ea905a29..00000000000 --- a/qa/spec/scenario/test/integration/oauth_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -describe QA::Scenario::Test::Integration::OAuth do - describe '#perform' do - it_behaves_like 'a QA scenario class' do - let(:tags) { [:oauth] } - end - end -end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 2d6d8d71917..c011a9e3bb4 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -126,6 +126,26 @@ FactoryBot.define do end end + factory :open_project_service do + project + active { true } + + transient do + url { 'http://openproject.example.com' } + api_url { 'http://openproject.example.com/issues/:id' } + token { 'supersecret' } + closed_status_id { '15' } + project_identifier_code { 'PRJ-1' } + end + + after(:build) do |service, evaluator| + create(:open_project_tracker_data, service: service, + url: evaluator.url, api_url: evaluator.api_url, token: evaluator.token, + closed_status_id: evaluator.closed_status_id, project_identifier_code: evaluator.project_identifier_code + ) + end + end + trait :jira_cloud_service do url { 'https://mysite.atlassian.net' } username { 'jira_user' } diff --git a/spec/factories/services_data.rb b/spec/factories/services_data.rb index 5a3639895b6..c62fff2af55 100644 --- a/spec/factories/services_data.rb +++ b/spec/factories/services_data.rb @@ -9,4 +9,12 @@ FactoryBot.define do factory :issue_tracker_data do service end + + factory :open_project_tracker_data do + service + url { 'http://openproject.example.com'} + token { 'supersecret' } + project_identifier_code { 'PRJ-1' } + closed_status_id { '15' } + end end diff --git a/spec/factories/terraform/state.rb b/spec/factories/terraform/state.rb new file mode 100644 index 00000000000..4b83128ff6e --- /dev/null +++ b/spec/factories/terraform/state.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :terraform_state, class: 'Terraform::State' do + project { create(:project) } + + trait :with_file do + file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate') } + end + end +end diff --git a/spec/fixtures/terraform/terraform.tfstate b/spec/fixtures/terraform/terraform.tfstate new file mode 100644 index 00000000000..3384d9eb005 --- /dev/null +++ b/spec/fixtures/terraform/terraform.tfstate @@ -0,0 +1,8 @@ +{ + "version": 4, + "terraform_version": "0.12.21", + "serial": 1, + "lineage": "25e05991-243d-28d6-ebe3-ee0baae462cf", + "outputs": {}, + "resources": [] +}
\ No newline at end of file diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js index ef97cb11424..7f893bf7ed7 100644 --- a/spec/frontend/__mocks__/@gitlab/ui.js +++ b/spec/frontend/__mocks__/@gitlab/ui.js @@ -17,3 +17,20 @@ export const GlTooltip = { return h('div', this.$attrs, this.$slots.default); }, }; + +export const GlPopoverDirective = { + bind() {}, +}; + +export const GlPopover = { + props: { + cssClasses: { + type: Array, + required: false, + default: () => [], + }, + }, + render(h) { + return h('div', this.$attrs, this.$slots.default); + }, +}; diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js new file mode 100644 index 00000000000..e39e6e7e2c2 --- /dev/null +++ b/spec/frontend/monitoring/components/charts/bar_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlBarChart } from '@gitlab/ui/dist/charts'; +import Bar from '~/monitoring/components/charts/bar.vue'; +import { barMockData } from '../../mock_data'; + +jest.mock('~/lib/utils/icon_utils', () => ({ + getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'), +})); + +describe('Bar component', () => { + let barChart; + let store; + + beforeEach(() => { + barChart = shallowMount(Bar, { + propsData: { + graphData: barMockData, + }, + store, + }); + }); + + afterEach(() => { + barChart.destroy(); + }); + + describe('wrapped components', () => { + describe('GitLab UI bar chart', () => { + let glbarChart; + let chartData; + + beforeEach(() => { + glbarChart = barChart.find(GlBarChart); + chartData = barChart.vm.chartData[barMockData.metrics[0].label]; + }); + + it('is a Vue instance', () => { + expect(glbarChart.isVueInstance()).toBe(true); + }); + + it('should display a label on the x axis', () => { + expect(glbarChart.vm.xAxisTitle).toBe(barMockData.xLabel); + }); + + it('should return chartData as array of arrays', () => { + expect(chartData).toBeInstanceOf(Array); + + chartData.forEach(item => { + expect(item).toBeInstanceOf(Array); + }); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index c98b6a9592f..284b7a0997f 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -703,3 +703,50 @@ export const stackedColumnMockedData = { }, ], }; + +export const barMockData = { + title: 'SLA Trends - Primary Services', + type: 'bar-chart', + xLabel: 'service', + y_label: 'percentile', + metrics: [ + { + id: 'sla_trends_primary_services', + series_name: 'group 1', + metric_id: 'undefined_sla_trends_primary_services', + metricId: 'undefined_sla_trends_primary_services', + query_range: + 'avg(avg_over_time(slo_observation_status{environment="gprd", stage=~"main|", type=~"api|web|git|registry|sidekiq|ci-runners"}[1d])) by (type)', + unit: 'Percentile', + label: 'SLA', + prometheus_endpoint_path: + '/gitlab-com/metrics-dogfooding/-/environments/266/prometheus/api/v1/query_range?query=clamp_min%28clamp_max%28avg%28avg_over_time%28slo_observation_status%7Benvironment%3D%22gprd%22%2C+stage%3D~%22main%7C%22%2C+type%3D~%22api%7Cweb%7Cgit%7Cregistry%7Csidekiq%7Cci-runners%22%7D%5B1d%5D%29%29+by+%28type%29%2C1%29%2C0%29', + result: [ + { + metric: { type: 'api' }, + values: [[1583995208, '0.9935198135198128']], + }, + { + metric: { type: 'git' }, + values: [[1583995208, '0.9975296513504401']], + }, + { + metric: { type: 'registry' }, + values: [[1583995208, '0.9994716394716395']], + }, + { + metric: { type: 'sidekiq' }, + values: [[1583995208, '0.9948251748251747']], + }, + { + metric: { type: 'web' }, + values: [[1583995208, '0.9535664335664336']], + }, + { + metric: { type: 'postgresql_database' }, + values: [[1583995208, '0.9335664335664336']], + }, + ], + }, + ], +}; diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 1e5bbc9c113..4bbbe33647c 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -25,6 +25,10 @@ describe('mapToDashboardViewModel', () => { panels: [ { title: 'Title A', + xLabel: '', + xAxis: { + name: '', + }, type: 'chart-type', y_label: 'Y Label A', metrics: [], @@ -44,6 +48,10 @@ describe('mapToDashboardViewModel', () => { { title: 'Title A', type: 'chart-type', + xLabel: '', + xAxis: { + name: '', + }, y_label: 'Y Label A', yAxis: { name: 'Y Label A', @@ -114,6 +122,28 @@ describe('mapToDashboardViewModel', () => { const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0]; + it('panel with x_label', () => { + setupWithPanel({ + title: panelTitle, + x_label: 'x label', + }); + + expect(getMappedPanel()).toEqual({ + title: panelTitle, + xLabel: 'x label', + xAxis: { + name: 'x label', + }, + y_label: '', + yAxis: { + name: '', + format: SUPPORTED_FORMATS.number, + precision: 2, + }, + metrics: [], + }); + }); + it('group y_axis defaults', () => { setupWithPanel({ title: panelTitle, @@ -121,7 +151,11 @@ describe('mapToDashboardViewModel', () => { expect(getMappedPanel()).toEqual({ title: panelTitle, + xLabel: '', y_label: '', + xAxis: { + name: '', + }, yAxis: { name: '', format: SUPPORTED_FORMATS.number, diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 2d9417bf971..262b8b985cc 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -6,6 +6,7 @@ import { graphDataPrometheusQuery, graphDataPrometheusQueryRange, anomalyMockGraphData, + barMockData, } from './mock_data'; jest.mock('~/lib/utils/url_utility'); @@ -210,4 +211,67 @@ describe('monitoring/utils', () => { expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: `${seconds}` }, fromUrl); }); }); + + describe('barChartsDataParser', () => { + const singleMetricExpected = { + SLA: [ + ['0.9935198135198128', 'api'], + ['0.9975296513504401', 'git'], + ['0.9994716394716395', 'registry'], + ['0.9948251748251747', 'sidekiq'], + ['0.9535664335664336', 'web'], + ['0.9335664335664336', 'postgresql_database'], + ], + }; + + const multipleMetricExpected = { + ...singleMetricExpected, + SLA_2: Object.values(singleMetricExpected)[0], + }; + + const barMockDataWithMultipleMetrics = { + ...barMockData, + metrics: [ + barMockData.metrics[0], + { + ...barMockData.metrics[0], + label: 'SLA_2', + }, + ], + }; + + [ + { + input: { metrics: undefined }, + output: {}, + testCase: 'barChartsDataParser returns {} with undefined', + }, + { + input: { metrics: null }, + output: {}, + testCase: 'barChartsDataParser returns {} with null', + }, + { + input: { metrics: [] }, + output: {}, + testCase: 'barChartsDataParser returns {} with []', + }, + { + input: barMockData, + output: singleMetricExpected, + testCase: 'barChartsDataParser returns single series object with single metrics', + }, + { + input: barMockDataWithMultipleMetrics, + output: multipleMetricExpected, + testCase: 'barChartsDataParser returns multiple series object with multiple metrics', + }, + ].forEach(({ input, output, testCase }) => { + it(testCase, () => { + expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual( + expect.objectContaining(output), + ); + }); + }); + }); }); diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb index 8863f26fbf2..26e44fa7cc8 100644 --- a/spec/lib/gitlab/auth/current_user_mode_spec.rb +++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb @@ -196,7 +196,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store subject.request_admin_mode! subject.enable_admin_mode!(password: user.password) - expect(session).to include(expected_session_entry(be_within(1.second).of Time.now)) + expect(session).to include(expected_session_entry(be_within(1.second).of(Time.now))) end end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index 243c6f06324..7a413a7aeac 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do describe '#children' do subject { |example| path(example).children } - it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to all(be_an_instance_of(described_class)) } it do is_expected.to contain_exactly entry('path/dir_1/file_1'), entry('path/dir_1/file_b'), @@ -78,7 +78,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do subject { |example| path(example).files } it { is_expected.to all(be_file) } - it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to all(be_an_instance_of(described_class)) } it do is_expected.to contain_exactly entry('path/dir_1/file_1'), entry('path/dir_1/file_b') @@ -90,7 +90,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do subject { |example| path(example).directories } it { is_expected.to all(be_directory) } - it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to all(be_an_instance_of(described_class)) } it { is_expected.to contain_exactly entry('path/dir_1/subdir/') } end @@ -98,7 +98,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do subject { |example| path(example).directories(parent: true) } it { is_expected.to all(be_directory) } - it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to all(be_an_instance_of(described_class)) } it do is_expected.to contain_exactly entry('path/dir_1/subdir/'), entry('path/') diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index b57764bceef..9e95d31f41c 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -69,7 +69,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#diffs' do subject { message.diffs } - it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) } + it { is_expected.to all(be_an_instance_of(Gitlab::Diff::File)) } end describe '#diffs_count' do diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index ebd7c7af265..0bf46217d60 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -74,7 +74,7 @@ describe Gitlab::Gfm::UploadsRewriter do end it 'throw an error' do - expect { rewriter.rewrite(new_project) }.to raise_error(an_instance_of(StandardError).and having_attributes(message: "Invalid path")) + expect { rewriter.rewrite(new_project) }.to raise_error(an_instance_of(StandardError).and(having_attributes(message: "Invalid path"))) end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 0ed5a1e7b49..9abc8a5d88f 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -252,6 +252,7 @@ services: - service_hook - jira_tracker_data - issue_tracker_data +- open_project_tracker_data hooks: - project - web_hook_logs diff --git a/spec/lib/gitlab/import_formatter_spec.rb b/spec/lib/gitlab/import_formatter_spec.rb new file mode 100644 index 00000000000..e9f63ba5777 --- /dev/null +++ b/spec/lib/gitlab/import_formatter_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportFormatter do + let(:formatter) { Gitlab::ImportFormatter.new } + + describe '#comment' do + it 'creates the correct string' do + expect(formatter.comment('Name', '2020-02-02', 'some text')).to eq( + "\n\n*By Name on 2020-02-02*\n\nsome text" + ) + end + end + + describe '#author_line' do + it 'returns the correct string with provided author name' do + expect(formatter.author_line('Name')).to eq("*Created by: Name*\n\n") + end + + it 'returns the correct string with Anonymous name if author not provided' do + expect(formatter.author_line(nil)).to eq("*Created by: Anonymous*\n\n") + end + end + + describe '#assignee_line' do + it 'returns the correct string with provided author name' do + expect(formatter.assignee_line('Name')).to eq("*Assigned to: Name*\n\n") + end + + it 'returns the correct string with Anonymous name if author not provided' do + expect(formatter.assignee_line(nil)).to eq("*Assigned to: Anonymous*\n\n") + end + end +end diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb new file mode 100644 index 00000000000..03631a3e941 --- /dev/null +++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::IssueSerializer do + describe '#execute' do + let_it_be(:project) { create(:project) } + + let(:iid) { 5 } + let(:key) { 'PROJECT-5' } + let(:summary) { 'some title' } + 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(:jira_status) { 'new' } + let(:jira_issue) do + double( + id: '1234', + key: key, + summary: summary, + description: description, + created: created_at, + updated: updated_at, + assignee: assignee, + reporter: double(displayName: 'Reporter'), + status: double(statusCategory: { 'key' => jira_status }) + ) + end + let(:params) { { iid: iid } } + + let(:expected_description) do + <<~MD + *Created by: Reporter* + + *Assigned to: Solver* + + basic description + MD + end + + subject { described_class.new(project, jira_issue, params).execute } + + context 'attributes setting' do + it 'sets the basic attributes' do + expect(subject).to eq( + iid: iid, + project_id: project.id, + description: expected_description.strip, + title: "[#{key}] #{summary}", + state_id: 1, + updated_at: updated_at, + created_at: created_at, + author_id: project.creator_id + ) + end + end + + context 'with done status' do + let(:jira_status) { 'done' } + + it 'maps the status to closed' do + expect(subject[:state_id]).to eq(2) + end + end + + context 'without the assignee' do + let(:assignee) { nil } + + it 'does not include assignee in the description' do + expected_description = <<~MD + *Created by: Reporter* + + basic description + MD + + expect(subject[:description]).to eq(expected_description.strip) + end + end + + context 'without the iid' do + let(:params) { {} } + + it 'does not set the iid' do + expect(subject[:iid]).to be_nil + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index c574cc91a59..d21efe2e1fe 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -230,7 +230,7 @@ describe Notify do is_expected.to have_referable_subject(issue, reply: true) is_expected.to have_body_text(status) is_expected.to have_body_text(current_user_sanitized) - is_expected.to have_body_text(project_issue_path project, issue) + is_expected.to have_body_text(project_issue_path(project, issue)) end end end diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb index 6cd3eb33d9b..12f6b99e8a7 100644 --- a/spec/models/project_services/jira_tracker_data_spec.rb +++ b/spec/models/project_services/jira_tracker_data_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe JiraTrackerData do - let(:service) { create(:jira_service, active: false, properties: {}) } + let(:service) { create(:jira_service, active: false) } describe 'Associations' do it { is_expected.to belong_to(:service) } diff --git a/spec/models/project_services/open_project_service_spec.rb b/spec/models/project_services/open_project_service_spec.rb new file mode 100644 index 00000000000..8e373a31e62 --- /dev/null +++ b/spec/models/project_services/open_project_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OpenProjectService do + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:url) } + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_presence_of(:project_identifier_code) } + + it_behaves_like 'issue tracker service URL attribute', :url + it_behaves_like 'issue tracker service URL attribute', :api_url + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:url) } + it { is_expected.not_to validate_presence_of(:token) } + it { is_expected.not_to validate_presence_of(:project_identifier_code) } + end + end + + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end +end diff --git a/spec/models/project_services/open_project_tracker_data_spec.rb b/spec/models/project_services/open_project_tracker_data_spec.rb new file mode 100644 index 00000000000..0d387bbf69b --- /dev/null +++ b/spec/models/project_services/open_project_tracker_data_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OpenProjectTrackerData do + describe 'Associations' do + it { is_expected.to belong_to(:service) } + end + + describe 'closed_status_id' do + it 'returns the set value' do + expect(build(:open_project_tracker_data).closed_status_id).to eq('15') + end + + it 'returns the default value if not set' do + expect(build(:open_project_tracker_data, closed_status_id: nil).closed_status_id).to eq('13') + end + end +end diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb new file mode 100644 index 00000000000..1d677e7ece5 --- /dev/null +++ b/spec/models/terraform/state_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Terraform::State do + subject { create(:terraform_state, :with_file) } + + it { is_expected.to belong_to(:project) } + + it { is_expected.to validate_presence_of(:project_id) } + + before do + stub_terraform_state_object_storage(Terraform::StateUploader) + end + + describe '#file_store' do + context 'when no value is set' do + it 'returns the default store of the uploader' do + [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| + expect(Terraform::StateUploader).to receive(:default_store).and_return(store) + expect(described_class.new.file_store).to eq(store) + end + end + end + + context 'when a value is set' do + it 'returns the value' do + [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| + expect(build(:terraform_state, file_store: store).file_store).to eq(store) + end + end + end + end + + describe '#update_file_store' do + context 'when file is stored in object storage' do + it 'sets file_store to remote' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'when file is stored locally' do + before do + stub_terraform_state_object_storage(Terraform::StateUploader, enabled: false) + end + + it 'sets file_store to local' do + expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index d8fac47d6f6..a3fd365771b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -910,13 +910,13 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.reload.iid}", user) expect(json_response['merged_by']['id']).to eq(merge_request.metrics.merged_by_id) - expect(Time.parse json_response['merged_at']).to be_like_time(merge_request.metrics.merged_at) + expect(Time.parse(json_response['merged_at'])).to be_like_time(merge_request.metrics.merged_at) expect(json_response['closed_by']['id']).to eq(merge_request.metrics.latest_closed_by_id) - expect(Time.parse json_response['closed_at']).to be_like_time(merge_request.metrics.latest_closed_at) + expect(Time.parse(json_response['closed_at'])).to be_like_time(merge_request.metrics.latest_closed_at) expect(json_response['pipeline']['id']).to eq(merge_request.metrics.pipeline_id) - expect(Time.parse json_response['latest_build_started_at']).to be_like_time(merge_request.metrics.latest_build_started_at) - expect(Time.parse json_response['latest_build_finished_at']).to be_like_time(merge_request.metrics.latest_build_finished_at) - expect(Time.parse json_response['first_deployed_to_production_at']).to be_like_time(merge_request.metrics.first_deployed_to_production_at) + expect(Time.parse(json_response['latest_build_started_at'])).to be_like_time(merge_request.metrics.latest_build_started_at) + expect(Time.parse(json_response['latest_build_finished_at'])).to be_like_time(merge_request.metrics.latest_build_finished_at) + expect(Time.parse(json_response['first_deployed_to_production_at'])).to be_like_time(merge_request.metrics.first_deployed_to_production_at) end end diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb index 974f72fa376..4aefe5f7dae 100644 --- a/spec/services/merge_requests/after_create_service_spec.rb +++ b/spec/services/merge_requests/after_create_service_spec.rb @@ -53,19 +53,5 @@ describe MergeRequests::AfterCreateService do after_create_service.execute(merge_request) end - - # https://gitlab.com/gitlab-org/gitlab/issues/208813 - context 'when the create_merge_request_pipelines_in_sidekiq flag is disabled' do - before do - stub_feature_flags(create_merge_request_pipelines_in_sidekiq: false) - end - - it 'does not create a pipeline or update the HEAD pipeline' do - expect(after_create_service).not_to receive(:create_pipeline_for) - expect(merge_request).not_to receive(:update_head_pipeline) - - after_create_service.execute(merge_request) - end - end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 2514fda5053..dc34546a599 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -129,22 +129,6 @@ describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end end - # https://gitlab.com/gitlab-org/gitlab/issues/208813 - context 'when the create_merge_request_pipelines_in_sidekiq flag is disabled' do - before do - stub_feature_flags(create_merge_request_pipelines_in_sidekiq: false) - end - - it 'creates a pipeline and updates the HEAD pipeline' do - expect(service).to receive(:create_pipeline_for) - expect_next_instance_of(MergeRequest) do |merge_request| - expect(merge_request).to receive(:update_head_pipeline) - end - - service.execute - end - end - context 'when head pipelines already exist for merge request source branch', :sidekiq_inline do let(:shas) { project.repository.commits(opts[:source_branch], limit: 2).map(&:id) } let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[1]) } diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb index 6ded57f961c..92667184be8 100644 --- a/spec/services/projects/group_links/create_service_spec.rb +++ b/spec/services/projects/group_links/create_service_spec.rb @@ -27,6 +27,6 @@ describe Projects::GroupLinks::CreateService, '#execute' do end it 'returns error if user is not allowed to share with a group' do - expect { subject.execute(create :group) }.not_to change { project.project_group_links.count } + expect { subject.execute(create(:group)) }.not_to change { project.project_group_links.count } end end diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index b624b9475e3..255f044db90 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -160,7 +160,7 @@ describe Releases::CreateService do context 'when no milestone is passed in' do it 'creates a release without a milestone tied to it' do - expect(params.key? :milestones).to be_falsey + expect(params.key?(:milestones)).to be_falsey service.execute release = project.releases.last diff --git a/spec/services/spam/spam_check_service_spec.rb b/spec/services/spam/spam_check_service_spec.rb index 732b64b52a0..8ae6764c5d5 100644 --- a/spec/services/spam/spam_check_service_spec.rb +++ b/spec/services/spam/spam_check_service_spec.rb @@ -103,7 +103,7 @@ describe Spam::SpamCheckService do issue.description = 'SPAM!' end - context 'when indicated as spam by akismet' do + context 'when indicated as spam by Akismet' do before do allow(Spam::AkismetService).to receive(:new).and_return(double(spam?: true)) end @@ -115,7 +115,7 @@ describe Spam::SpamCheckService do it_behaves_like 'akismet spam' - it 'checks as spam' do + it 'marks as spam' do subject expect(issue.reload.spam).to be_truthy @@ -125,7 +125,7 @@ describe Spam::SpamCheckService do context 'when allow_possible_spam feature flag is true' do it_behaves_like 'akismet spam' - it 'does not check as spam' do + it 'does not mark as spam' do subject expect(issue.spam).to be_falsey @@ -133,7 +133,7 @@ describe Spam::SpamCheckService do end end - context 'when not indicated as spam by akismet' do + context 'when not indicated as spam by Akismet' do before do allow(Spam::AkismetService).to receive(:new).and_return(double(spam?: false)) end diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb index 3eb486833e6..ece714ee8e5 100644 --- a/spec/services/wiki_pages/update_service_spec.rb +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -94,7 +94,7 @@ describe WikiPages::UpdateService do end it 'reports the error' do - expect(service.execute page).to be_invalid + expect(service.execute(page)).to be_invalid .and have_attributes(errors: be_present) end end diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 392300a4436..d4ac286e959 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -70,6 +70,13 @@ module StubObjectStorage **params) end + def stub_terraform_state_object_storage(uploader = described_class, **params) + stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, + uploader: uploader, + remote_directory: 'terraform_state', + **params) + end + def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id") stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z}) .to_return status: 200, body: <<-EOS.strip_heredoc diff --git a/spec/uploaders/terraform/state_uploader_spec.rb b/spec/uploaders/terraform/state_uploader_spec.rb new file mode 100644 index 00000000000..4577a2c4738 --- /dev/null +++ b/spec/uploaders/terraform/state_uploader_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Terraform::StateUploader do + subject { terraform_state.file } + + let(:terraform_state) { create(:terraform_state, file: fixture_file_upload('spec/fixtures/terraform/terraform.tfstate')) } + + before do + stub_terraform_state_object_storage + end + + describe '#filename' do + it 'contains the ID of the terraform state record' do + expect(subject.filename).to include(terraform_state.id.to_s) + end + end + + describe '#store_dir' do + it 'contains the ID of the project' do + expect(subject.store_dir).to include(terraform_state.project_id.to_s) + end + end + + describe '#key' do + it 'creates a digest with a secret key and the project id' do + expect(OpenSSL::HMAC) + .to receive(:digest) + .with('SHA256', Gitlab::Application.secrets.db_key_base, terraform_state.project_id.to_s) + .and_return('digest') + + expect(subject.key).to eq('digest') + end + end + + describe 'encryption' do + it 'encrypts the stored file' do + expect(subject.file.read).not_to eq(fixture_file('terraform/terraform.tfstate')) + end + + it 'decrypts the file when reading' do + expect(subject.read).to eq(fixture_file('terraform/terraform.tfstate')) + end + end + + describe '.direct_upload_enabled?' do + it 'returns false' do + expect(described_class.direct_upload_enabled?).to eq(false) + end + end + + describe '.background_upload_enabled?' do + it 'returns false' do + expect(described_class.background_upload_enabled?).to eq(false) + end + end + + describe '.proxy_download_enabled?' do + it 'returns true' do + expect(described_class.proxy_download_enabled?).to eq(true) + end + end + + describe '.default_store' do + context 'when object storage is enabled' do + it 'returns REMOTE' do + expect(described_class.default_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'when object storage is disabled' do + before do + stub_terraform_state_object_storage(enabled: false) + end + + it 'returns LOCAL' do + expect(described_class.default_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end +end |