diff options
author | John T Skarbek <jskarbek@gitlab.com> | 2019-04-30 18:58:48 -0400 |
---|---|---|
committer | John T Skarbek <jskarbek@gitlab.com> | 2019-04-30 18:58:48 -0400 |
commit | b1e7c4c6f1baedec623e5ef8b898881e76c84e81 (patch) | |
tree | 6a1f42fdef31ec4a4f28e5fabeb50488d6287231 | |
parent | 6900142d1bd374c14ff98e7ea91159a560daa37b (diff) | |
parent | 109002071858acb39dc00c8e5652d14f5f4d886d (diff) | |
download | gitlab-ce-b1e7c4c6f1baedec623e5ef8b898881e76c84e81.tar.gz |
Merge remote-tracking branch 'origin/master'
213 files changed, 2972 insertions, 861 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a2d87226ac2..39fc130ef85 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.35.0
\ No newline at end of file +1.36.0 @@ -285,7 +285,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.3' gem 'gettext', '~> 3.2.2', require: false, group: :development -gem 'batch-loader', '~> 1.2.2' +gem 'batch-loader', '~> 1.4.0' # Perf bar gem 'peek', '~> 1.0.1' @@ -309,7 +309,7 @@ group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false - gem 'letter_opener_web', '~> 1.3.0' + gem 'letter_opener_web', '~> 1.3.4' gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false # Better errors handler @@ -417,7 +417,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.22.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.26.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index ceece1da8d7..3b03a8ef691 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,7 @@ GEM thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) base32 (0.3.2) - batch-loader (1.2.2) + batch-loader (1.4.0) bcrypt (3.1.12) bcrypt_pbkdf (1.0.0) benchmark-ips (2.3.0) @@ -283,7 +283,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.22.0) + gitaly-proto (1.26.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -444,9 +444,9 @@ GEM rest-client (~> 2.0) launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.4.1) + letter_opener (1.7.0) launchy (~> 2.2) - letter_opener_web (1.3.0) + letter_opener_web (1.3.4) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -999,7 +999,7 @@ DEPENDENCIES awesome_print babosa (~> 1.0.2) base32 (~> 0.3.0) - batch-loader (~> 1.2.2) + batch-loader (~> 1.4.0) bcrypt_pbkdf (~> 1.0) benchmark-ips (~> 2.3.0) better_errors (~> 2.5.0) @@ -1056,7 +1056,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.22.0) + gitaly-proto (~> 1.26.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-labkit (~> 0.1.2) @@ -1093,7 +1093,7 @@ DEPENDENCIES kaminari (~> 1.0) knapsack (~> 1.17) kubeclient (~> 4.2.2) - letter_opener_web (~> 1.3.0) + letter_opener_web (~> 1.3.4) license_finder (~> 5.4) licensee (~> 8.9) lograge (~> 0.5) diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js index b88e69a07bf..2e537d8c000 100644 --- a/app/assets/javascripts/blob/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -1,8 +1,9 @@ import Flash from '../flash'; import BalsamiqViewer from './balsamiq/balsamiq_viewer'; +import { __ } from '~/locale'; function onError() { - const flash = new Flash('Balsamiq file could not be loaded.'); + const flash = new Flash(__('Balsamiq file could not be loaded.')); return flash; } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index cd3251ad1ca..9010cd0c3c1 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -5,6 +5,7 @@ import Dropzone from 'dropzone'; import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; +import { sprintf, __ } from '~/locale'; Dropzone.autoDiscover = false; @@ -73,7 +74,7 @@ export default class BlobFileDropzone { .html(errorMessage) .text(); $('.dropzone-alerts') - .html(`Error uploading file: "${stripped}"`) + .html(sprintf(__('Error uploading file: %{stripped}'), { stripped })) .show(); this.removeFile(file); }, @@ -84,7 +85,7 @@ export default class BlobFileDropzone { e.stopPropagation(); if (dropzone[0].dropzone.getQueuedFiles().length === 0) { // eslint-disable-next-line no-alert - alert('Please select a file'); + alert(__('Please select a file')); return false; } toggleLoading(submitButton, submitButtonLoadingIcon, true); diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js index 57c1baa9886..dbff03dc734 100644 --- a/app/assets/javascripts/blob/sketch/index.js +++ b/app/assets/javascripts/blob/sketch/index.js @@ -1,5 +1,6 @@ import JSZip from 'jszip'; import JSZipUtils from 'jszip-utils'; +import { __ } from '~/locale'; export default class SketchLoader { constructor(container) { @@ -56,10 +57,10 @@ export default class SketchLoader { const errorMsg = document.createElement('p'); errorMsg.className = 'prepend-top-default append-bottom-default text-center'; - errorMsg.textContent = ` + errorMsg.textContent = __(` Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above. - `; + `); this.container.appendChild(errorMsg); this.removeLoadingIcon(); diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 4718b642617..659d57e6a6f 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,11 +1,12 @@ import FileTemplateSelector from '../file_template_selector'; +import { __ } from '~/locale'; export default class DockerfileSelector extends FileTemplateSelector { constructor({ mediator }) { super(mediator); this.config = { key: 'dockerfile', - name: 'Dockerfile', + name: __('Dockerfile'), pattern: /(Dockerfile)/, type: 'dockerfiles', dropdown: '.js-dockerfile-selector', diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index d0359fc5fe9..d246a1f6064 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Flash from '../../flash'; import { handleLocationHash } from '../../lib/utils/common_utils'; import axios from '../../lib/utils/axios_utils'; +import { __ } from '~/locale'; export default class BlobViewer { constructor() { @@ -26,7 +27,7 @@ export default class BlobViewer { promise .then(module => module.default(viewer)) .catch(error => { - Flash('Error loading file viewer.'); + Flash(__('Error loading file viewer.')); throw error; }); @@ -106,16 +107,19 @@ export default class BlobViewer { if (!this.copySourceBtn) return; if (this.simpleViewer.getAttribute('data-loaded')) { - this.copySourceBtn.setAttribute('title', 'Copy source to clipboard'); + this.copySourceBtn.setAttribute('title', __('Copy source to clipboard')); this.copySourceBtn.classList.remove('disabled'); } else if (this.activeViewer === this.simpleViewer) { this.copySourceBtn.setAttribute( 'title', - 'Wait for the source to load to copy it to the clipboard', + __('Wait for the source to load to copy it to the clipboard'), ); this.copySourceBtn.classList.add('disabled'); } else { - this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard'); + this.copySourceBtn.setAttribute( + 'title', + __('Switch to the source to copy it to the clipboard'), + ); this.copySourceBtn.classList.add('disabled'); } @@ -158,7 +162,7 @@ export default class BlobViewer { this.toggleCopyButtonState(); }) - .catch(() => new Flash('Error loading viewer')); + .catch(() => new Flash(__('Error loading viewer'))); } static loadViewer(viewerParam) { diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index c9effa0639b..b8882203cc7 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -83,7 +83,7 @@ export default { }" :index="index" :data-issue-id="issue.id" - class="board-card position-relative p-3 rounded" + class="board-card p-3 rounded" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="showIssue($event)" diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 4f47f1b6550..8461e01de7b 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -279,14 +279,10 @@ export default class Clusters { this.store.acknowledgeSuccessfulUpdate(appId); } - toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { - const { externalIp, status } = ingressNewState; - const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp; - const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`; - - if (ingressPreviousState.status !== status) { - this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden); - this.ingressDomainSnippet.textContent = domainSnippetText; + toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) { + if (externalIp !== newExternalIp) { + this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp); + this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`; } } diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 4de425b48e7..3f0a9f2602c 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -12,6 +12,7 @@ import stageStagingComponent from './components/stage_staging_component.vue'; import stageTestComponent from './components/stage_test_component.vue'; import CycleAnalyticsService from './cycle_analytics_service'; import CycleAnalyticsStore from './cycle_analytics_store'; +import { __ } from '~/locale'; Vue.use(Translate); @@ -61,7 +62,7 @@ export default () => { methods: { handleError() { this.store.setErrorState(true); - return new Flash('There was an error while fetching cycle analytics data.'); + return new Flash(__('There was an error while fetching cycle analytics data.')); }, initDropdown() { const $dropdown = $('.js-ca-dropdown'); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 518a9cf7a0f..8c84b98a108 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -3,6 +3,7 @@ import VueRouter from 'vue-router'; import { joinPaths } from '~/lib/utils/url_utility'; import flash from '~/flash'; import store from './stores'; +import { __ } from '~/locale'; Vue.use(VueRouter); @@ -94,7 +95,7 @@ router.beforeEach((to, from, next) => { }) .catch(e => { flash( - 'Error while loading the project data. Please try again.', + __('Error while loading the project data. Please try again.'), 'alert', document, null, diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js index 628babe6a01..f10891a8e5b 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -1,4 +1,5 @@ import { activityBarViews } from '../../../constants'; +import { __ } from '~/locale'; export const templateTypes = () => [ { @@ -10,11 +11,11 @@ export const templateTypes = () => [ key: 'gitignores', }, { - name: 'LICENSE', + name: __('LICENSE'), key: 'licenses', }, { - name: 'Dockerfile', + name: __('Dockerfile'), key: 'dockerfiles', }, ]; diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js index 547c078ec55..f7e80950803 100644 --- a/app/assets/javascripts/mirrors/ssh_mirror.js +++ b/app/assets/javascripts/mirrors/ssh_mirror.js @@ -290,7 +290,7 @@ export default class SSHMirror { this.setSSHPublicKey(data.import_data_attributes.ssh_public_key); }) .catch(() => { - Flash(_('Unable to regenerate public ssh key.')); + Flash(__('Unable to regenerate public ssh key.')); }); } diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 5fcc2c8cfac..1efa5189996 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -1,7 +1,7 @@ import axios from '../../lib/utils/axios_utils'; import statusCodes from '../../lib/utils/http_status'; import { backOff } from '../../lib/utils/common_utils'; -import { s__ } from '../../locale'; +import { s__, __ } from '../../locale'; const MAX_REQUESTS = 3; @@ -15,7 +15,7 @@ function backOffRequest(makeRequestCallback) { if (requestCounter < MAX_REQUESTS) { next(); } else { - stop(new Error('Failed to connect to the prometheus server')); + stop(new Error(__('Failed to connect to the prometheus server'))); } } else { stop(resp); diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js index 433df844c80..c13c417cc18 100644 --- a/app/assets/javascripts/mr_popover/constants.js +++ b/app/assets/javascripts/mr_popover/constants.js @@ -1,10 +1,12 @@ +import { __ } from '~/locale'; + export const mrStates = { merged: 'merged', closed: 'closed', }; export const humanMRStates = { - merged: 'Merged', - closed: 'Closed', - open: 'Open', + merged: __('Merged'), + closed: __('Closed'), + open: __('Open'), }; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 2164e386fdb..ea82ff4e340 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; import { slugifyWithHyphens } from '../lib/utils/text_utility'; +import { s__ } from '~/locale'; let hasUserDefinedProjectPath = false; @@ -114,71 +115,71 @@ const bindEvents = () => { const value = $(this).val(); const templates = { rails: { - text: 'Ruby on Rails', + text: s__('ProjectTemplates|Ruby on Rails'), icon: '.template-option .icon-rails', }, express: { - text: 'NodeJS Express', + text: s__('ProjectTemplates|NodeJS Express'), icon: '.template-option .icon-express', }, spring: { - text: 'Spring', + text: s__('ProjectTemplates|Spring'), icon: '.template-option .icon-spring', }, iosswift: { - text: 'iOS (Swift)', + text: s__('ProjectTemplates|iOS (Swift)'), icon: '.template-option svg.icon-gitlab', }, dotnetcore: { - text: '.NET Core', + text: s__('ProjectTemplates|.NET Core'), icon: '.template-option .icon-dotnet', }, android: { - text: 'Android', + text: s__('ProjectTemplates|Android'), icon: '.template-option svg.icon-android', }, gomicro: { - text: 'Go Micro', + text: s__('ProjectTemplates|Go Micro'), icon: '.template-option .icon-gomicro', }, hugo: { - text: 'Pages/Hugo', + text: s__('ProjectTemplates|Pages/Hugo'), icon: '.template-option .icon-hugo', }, jekyll: { - text: 'Pages/Jekyll', + text: s__('ProjectTemplates|Pages/Jekyll'), icon: '.template-option .icon-jekyll', }, plainhtml: { - text: 'Pages/Plain HTML', + text: s__('ProjectTemplates|Pages/Plain HTML'), icon: '.template-option .icon-plainhtml', }, gitbook: { - text: 'Pages/GitBook', + text: s__('ProjectTemplates|Pages/GitBook'), icon: '.template-option .icon-gitbook', }, hexo: { - text: 'Pages/Hexo', + text: s__('ProjectTemplates|Pages/Hexo'), icon: '.template-option .icon-hexo', }, nfhugo: { - text: 'Netlify/Hugo', + text: s__('ProjectTemplates|Netlify/Hugo'), icon: '.template-option .icon-netlify', }, nfjekyll: { - text: 'Netlify/Jekyll', + text: s__('ProjectTemplates|Netlify/Jekyll'), icon: '.template-option .icon-netlify', }, nfplainhtml: { - text: 'Netlify/Plain HTML', + text: s__('ProjectTemplates|Netlify/Plain HTML'), icon: '.template-option .icon-netlify', }, nfgitbook: { - text: 'Netlify/GitBook', + text: s__('ProjectTemplates|Netlify/GitBook'), icon: '.template-option .icon-netlify', }, nfhexo: { - text: 'Netlify/Hexo', + text: s__('ProjectTemplates|Netlify/Hexo'), icon: '.template-option .icon-netlify', }, }; diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js index edc2293915f..4dd0175e528 100644 --- a/app/assets/javascripts/raven/index.js +++ b/app/assets/javascripts/raven/index.js @@ -4,8 +4,11 @@ const index = function index() { RavenConfig.init({ sentryDsn: gon.sentry_dsn, currentUserId: gon.current_user_id, - whitelistUrls: [gon.gitlab_url], - isProduction: process.env.NODE_ENV, + whitelistUrls: + process.env.NODE_ENV === 'production' + ? [gon.gitlab_url] + : [gon.gitlab_url, 'webpack-internal://'], + environment: gon.sentry_environment, release: gon.revision, tags: { revision: gon.revision, diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index 338006ce2b9..b4a8c263954 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -61,7 +61,7 @@ const RavenConfig = { release: this.options.release, tags: this.options.tags, whitelistUrls: this.options.whitelistUrls, - environment: this.options.isProduction ? 'production' : 'development', + environment: this.options.environment, ignoreErrors: this.IGNORE_ERRORS, ignoreUrls: this.IGNORE_URLS, shouldSendCallback: this.shouldSendSample.bind(this), diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 1a98564ff55..ca0fc0700ad 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export default class U2FError { constructor(errorCode, u2fFlowType) { this.errorCode = errorCode; @@ -8,15 +10,17 @@ export default class U2FError { message() { if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { - return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; + return __( + 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.', + ); } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) { if (this.u2fFlowType === 'authenticate') { - return 'This device has not been registered with us.'; + return __('This device has not been registered with us.'); } if (this.u2fFlowType === 'register') { - return 'This device has already been registered with us.'; + return __('This device has already been registered with us.'); } } - return 'There was a problem communicating with your device.'; + return __('There was a problem communicating with your device.'); } } diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index f01a51da0b3..ba63683f5c0 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -1,10 +1,12 @@ +import { __ } from '~/locale'; + const viewers = { image: { id: 'image', }, markdown: { id: 'markdown', - previewTitle: 'Preview Markdown', + previewTitle: __('Preview Markdown'), }, }; diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 549d27e96d9..2d1f7a1cfd0 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import '~/commons/bootstrap'; export default { bind(el) { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0e4b40b2bed..09ff518bbdf 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -210,6 +210,7 @@ border: 1px solid $gray-200; box-shadow: 0 1px 2px $issue-boards-card-shadow; line-height: $gl-padding; + list-style: none; &:not(:last-child) { margin-bottom: $gl-padding-8; diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb index 73c744efeba..16c2365f85d 100644 --- a/app/controllers/clusters/applications_controller.rb +++ b/app/controllers/clusters/applications_controller.rb @@ -4,6 +4,7 @@ class Clusters::ApplicationsController < Clusters::BaseController before_action :cluster before_action :authorize_create_cluster!, only: [:create] before_action :authorize_update_cluster!, only: [:update] + before_action :authorize_admin_cluster!, only: [:destroy] def create request_handler do @@ -21,6 +22,14 @@ class Clusters::ApplicationsController < Clusters::BaseController end end + def destroy + request_handler do + Clusters::Applications::DestroyService + .new(@cluster, current_user, cluster_application_destroy_params) + .execute(request) + end + end + private def request_handler @@ -40,4 +49,8 @@ class Clusters::ApplicationsController < Clusters::BaseController def cluster_application_params params.permit(:application, :hostname, :email) end + + def cluster_application_destroy_params + params.permit(:application) + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 2ef5c207d67..b4d89db20c5 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -132,18 +132,6 @@ class Projects::IssuesController < Projects::ApplicationController render_conflict_response end - def referenced_merge_requests - @merge_requests, @closed_by_merge_requests = ::Issues::ReferencedMergeRequestsService.new(project, current_user).execute(issue) - - respond_to do |format| - format.json do - render json: { - html: view_to_html_string('projects/issues/_merge_requests') - } - end - end - end - def related_branches @related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 48f4d7a586d..e88c46144ef 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -36,10 +36,10 @@ class ProjectsController < Projects::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def new - namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] - return access_denied! if namespace && !can?(current_user, :create_projects, namespace) + @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] + return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace) - @project = Project.new(namespace_id: namespace&.id) + @project = Project.new(namespace_id: @namespace&.id) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index 0f1a64b6c58..972f318c806 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -7,14 +7,14 @@ module Resolvers prepended do argument :full_path, GraphQL::ID_TYPE, required: true, - description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"' + description: 'The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-ce"' end def model_by_full_path(model, full_path) BatchLoader.for(full_path).batch(key: model) do |full_paths, loader, args| # `with_route` avoids an N+1 calculating full_path - args[:key].where_full_path_in(full_paths).with_route.each do |project| - loader.call(project.full_path, project) + args[:key].where_full_path_in(full_paths).with_route.each do |model_instance| + loader.call(model_instance.full_path, model_instance) end end end diff --git a/app/graphql/resolvers/group_resolver.rb b/app/graphql/resolvers/group_resolver.rb new file mode 100644 index 00000000000..4260e18829e --- /dev/null +++ b/app/graphql/resolvers/group_resolver.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Resolvers + class GroupResolver < BaseResolver + prepend FullPathResolver + + type Types::GroupType, null: true + + def resolve(full_path:) + model_by_full_path(Group, full_path) + end + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb new file mode 100644 index 00000000000..a2d615ee732 --- /dev/null +++ b/app/graphql/types/group_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class GroupType < NamespaceType + graphql_name 'Group' + + authorize :read_group + + expose_permissions Types::PermissionTypes::Group + + field :web_url, GraphQL::STRING_TYPE, null: true + + field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (group, args, ctx) do + group.avatar_url(only_path: false) + end + + if ::Group.supports_nested_objects? + field :parent, GroupType, null: true + end + end +end diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb new file mode 100644 index 00000000000..36d8ee8c878 --- /dev/null +++ b/app/graphql/types/namespace_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + class NamespaceType < BaseObject + graphql_name 'Namespace' + + field :id, GraphQL::ID_TYPE, null: false + + field :name, GraphQL::STRING_TYPE, null: false + field :path, GraphQL::STRING_TYPE, null: false + field :full_name, GraphQL::STRING_TYPE, null: false + field :full_path, GraphQL::ID_TYPE, null: false + + field :description, GraphQL::STRING_TYPE, null: true + field :visibility, GraphQL::STRING_TYPE, null: true + field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled? + field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true + end +end diff --git a/app/graphql/types/permission_types/group.rb b/app/graphql/types/permission_types/group.rb new file mode 100644 index 00000000000..29833993ce6 --- /dev/null +++ b/app/graphql/types/permission_types/group.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Group < BasePermissionType + graphql_name 'GroupPermissions' + + abilities :read_group + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index fbb4eddd13c..baea6658e05 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -66,6 +66,9 @@ module Types field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :namespace, Types::NamespaceType, null: false + field :group, Types::GroupType, null: true + field :merge_requests, Types::MergeRequestType.connection_type, null: true, diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 0f655ab9d03..40d7de1a49a 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -9,6 +9,11 @@ module Types resolver: Resolvers::ProjectResolver, description: "Find a project" + field :group, Types::GroupType, + null: true, + resolver: Resolvers::GroupResolver, + description: "Find a group" + field :metadata, Types::MetadataType, null: true, resolver: Resolvers::MetadataResolver, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index b413ffddb9d..557215ff4dc 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -183,6 +183,22 @@ module ApplicationSettingImplementation clientside_sentry_dsn.strip! if clientside_sentry_dsn.present? end + def sentry_enabled + Gitlab.config.sentry.enabled || read_attribute(:sentry_enabled) + end + + def sentry_dsn + Gitlab.config.sentry.dsn || read_attribute(:sentry_dsn) + end + + def clientside_sentry_enabled + Gitlab.config.sentry.enabled || read_attribute(:clientside_sentry_enabled) + end + + def clientside_sentry_dsn + Gitlab.config.sentry.dsn || read_attribute(:clientside_sentry_dsn) + end + def performance_bar_allowed_group Group.find_by_id(performance_bar_allowed_group_id) end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 0d8d7d95791..644716ba8e7 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -4,6 +4,7 @@ module Ci class Bridge < CommitStatus include Ci::Processable include Ci::Contextable + include Ci::PipelineDelegator include Importable include AfterCommitQueue include HasRef @@ -13,8 +14,6 @@ module Ci belongs_to :trigger_request validates :ref, presence: true - delegate :merge_request_event?, to: :pipeline - def self.retry(bridge, current_user) raise NotImplementedError end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e5236051118..5a2ead41578 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -6,6 +6,7 @@ module Ci include Ci::Processable include Ci::Metadatable include Ci::Contextable + include Ci::PipelineDelegator include TokenAuthenticatable include AfterCommitQueue include ObjectStorage::BackgroundMove @@ -49,8 +50,6 @@ module Ci delegate :terminal_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true - delegate :merge_request_event?, :merge_request_ref?, - :legacy_detached_merge_request_pipeline?, to: :pipeline ## # Since Gitlab 11.5, deployments records started being created right after diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bbd21eb0e78..2b7835d7fab 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -759,6 +759,18 @@ module Ci user == current_user end + def source_ref + if triggered_by_merge_request? + merge_request.source_branch + else + ref + end + end + + def source_ref_slug + Gitlab::Utils.slugify(source_ref.to_s) + end + private def ci_yaml_from_repo diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index ac0e7eb03bc..d6a7d1d2bdd 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -24,6 +24,12 @@ module Clusters 'stable/cert-manager' end + # We will implement this in future MRs. + # Need to reverse postinstall step + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: 'certmanager', diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 71aff00077d..a83d06c4b00 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -29,6 +29,13 @@ module Clusters self.status = 'installable' if cluster&.platform_kubernetes_active? end + # We will implement this in future MRs. + # Basically we need to check all other applications are not installed + # first. + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InitCommand.new( name: name, diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 376d54aab2c..a1023f44049 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -35,6 +35,13 @@ module Clusters 'stable/nginx-ingress' end + # We will implement this in future MRs. + # Basically we need to check all dependent applications are not installed + # first. + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index f86ff3551a1..987c057ad6d 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -38,6 +38,12 @@ module Clusters content_values.to_yaml end + # Will be addressed in future MRs + # We need to investigate and document what will be permenantly deleted. + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 38cbc9ce8eb..9fbf5d8af04 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -51,6 +51,12 @@ module Clusters { "domain" => hostname }.to_yaml end + # Handled in a new issue: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/59369 + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 954c29da196..a6b7617b830 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -16,10 +16,12 @@ module Clusters default_value_for :version, VERSION + after_destroy :disable_prometheus_integration + state_machine :status do after_transition any => [:installed] do |application| application.cluster.projects.each do |project| - project.find_or_initialize_service('prometheus').update(active: true) + project.find_or_initialize_service('prometheus').update!(active: true) end end end @@ -47,6 +49,14 @@ module Clusters ) end + def uninstall_command + Gitlab::Kubernetes::Helm::DeleteCommand.new( + name: name, + rbac: cluster.platform_kubernetes_rbac?, + files: files + ) + end + def upgrade_command(values) ::Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, @@ -82,6 +92,12 @@ module Clusters private + def disable_prometheus_integration + cluster.projects.each do |project| + project.prometheus_service&.update!(active: false) + end + end + def kube_client cluster&.kubeclient&.core_client end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 3ebf7a5cfba..af648db3708 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -29,6 +29,13 @@ module Clusters content_values.to_yaml end + # Need to investigate if pipelines run by this runner will stop upon the + # executor pod stopping + # I.e.run a pipeline, and uninstall runner while pipeline is running + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index ee964fb7c93..4514498b84b 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -18,6 +18,16 @@ module Clusters self.status = 'installable' if cluster&.application_helm_available? end + def can_uninstall? + allowed_to_uninstall? + end + + # All new applications should uninstall by default + # Override if there's dependencies that needs to be uninstalled first + def allowed_to_uninstall? + true + end + def self.application_name self.to_s.demodulize.underscore end diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 1273ed83abe..54a3dda6d75 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -25,9 +25,11 @@ module Clusters state :updating, value: 4 state :updated, value: 5 state :update_errored, value: 6 + state :uninstalling, value: 7 + state :uninstall_errored, value: 8 event :make_scheduled do - transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled + transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled end event :make_installing do @@ -40,8 +42,9 @@ module Clusters end event :make_errored do - transition any - [:updating] => :errored + transition any - [:updating, :uninstalling] => :errored transition [:updating] => :update_errored + transition [:uninstalling] => :uninstall_errored end event :make_updating do @@ -52,6 +55,10 @@ module Clusters transition any => :update_errored end + event :make_uninstalling do + transition [:scheduled] => :uninstalling + end + before_transition any => [:scheduled] do |app_status, _| app_status.status_reason = nil end @@ -65,7 +72,7 @@ module Clusters app_status.status_reason = nil end - before_transition any => [:update_errored] do |app_status, transition| + before_transition any => [:update_errored, :uninstall_errored] do |app_status, transition| status_reason = transition.args.first app_status.status_reason = status_reason if status_reason end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index a806367a49b..ca7d109d4f0 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -76,6 +76,10 @@ module Clusters end end + def namespace_for(project) + cluster.find_or_initialize_kubernetes_namespace_for_project(project).namespace + end + def predefined_variables(project:) Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'KUBE_URL', value: api_url) diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 4687ec7d166..80278e07e65 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -91,7 +91,8 @@ module Avatarable private def retrieve_upload_from_batch(identifier) - BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args| + BatchLoader.for(identifier: identifier, model: self) + .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args| model_class = args[:key] paths = upload_params.flat_map do |params| params[:model].upload_paths(params[:identifier]) diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index 4986a42dbd2..e1d5ce7f7d4 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -70,8 +70,8 @@ module Ci variables.append(key: 'CI_COMMIT_SHA', value: sha) variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha) variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha) - variables.append(key: 'CI_COMMIT_REF_NAME', value: ref) - variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug) + variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref) + variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug) variables.append(key: "CI_COMMIT_TAG", value: ref) if tag? variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? @@ -85,8 +85,8 @@ module Ci Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'CI_BUILD_REF', value: sha) variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha) - variables.append(key: 'CI_BUILD_REF_NAME', value: ref) - variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug) + variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref) + variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug) variables.append(key: 'CI_BUILD_NAME', value: name) variables.append(key: 'CI_BUILD_STAGE', value: stage) variables.append(key: "CI_BUILD_TAG", value: ref) if tag? diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb new file mode 100644 index 00000000000..dbc5ed1bc9a --- /dev/null +++ b/app/models/concerns/ci/pipeline_delegator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +## +# This module is mainly used by child associations of `Ci::Pipeline` that needs to look up +# single source of truth. For example, `Ci::Build` has `git_ref` method, which behaves +# slightly different from `Ci::Pipeline`'s `git_ref`. This is very confusing as +# the system could behave differently time to time. +# We should have a single interface in `Ci::Pipeline` and access the method always. +module Ci + module PipelineDelegator + extend ActiveSupport::Concern + + included do + delegate :merge_request_event?, + :merge_request_ref?, + :source_ref, + :source_ref_slug, + :legacy_detached_merge_request_pipeline?, to: :pipeline + end + end +end diff --git a/app/models/concerns/has_ref.rb b/app/models/concerns/has_ref.rb index 413cd36dcaa..fa0cf5ddfd2 100644 --- a/app/models/concerns/has_ref.rb +++ b/app/models/concerns/has_ref.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +## +# We will disable `ref` and `sha` attributes in `Ci::Build` in the future +# and remove this module in favor of Ci::PipelineDelegator. module HasRef extend ActiveSupport::Concern diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a5b62659b24..c2a1487fc6e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1054,6 +1054,16 @@ class MergeRequest < ApplicationRecord @environments[current_user] end + ## + # This method is for looking for active environments which created via pipelines for merge requests. + # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`), + # we cannot look up environments with source branch name. + def environments + return Environment.none unless actual_head_pipeline&.triggered_by_merge_request? + + actual_head_pipeline.environments + end + def state_human_name if merged? "Merged" diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index d73b2889f30..9e806b2e232 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -38,6 +38,8 @@ class PagesDomain < ApplicationRecord where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold)))) end + scope :for_removal, -> { where("remove_at < ?", Time.now) } + def verified? !!verified_at end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f650dbd3726..fc8afa9bead 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -94,6 +94,10 @@ class KubernetesService < DeploymentService end end + def namespace_for(project) + actual_namespace + end + # Check we can connect to the Kubernetes API def test(*args) kubeclient = build_kube_client! diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index 1c1347c5a57..944895904fe 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -63,19 +63,11 @@ module Ci end def link_to_merge_request_source_branch - return unless merge_request_presenter - - link_to(merge_request_presenter.source_branch, - merge_request_presenter.source_branch_commits_path, - class: 'ref-name') + merge_request_presenter&.source_branch_link end def link_to_merge_request_target_branch - return unless merge_request_presenter - - link_to(merge_request_presenter.target_branch, - merge_request_presenter.target_branch_commits_path, - class: 'ref-name') + merge_request_presenter&.target_branch_link end private diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 3f7b5bebb74..ba0711ca867 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -216,6 +216,22 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated help_page_path('ci/merge_request_pipelines/index.md') end + def source_branch_link + if source_branch_exists? + link_to(source_branch, source_branch_commits_path, class: 'ref-name') + else + content_tag(:span, source_branch, class: 'ref-name') + end + end + + def target_branch_link + if target_branch_exists? + link_to(target_branch, target_branch_commits_path, class: 'ref-name') + else + content_tag(:span, target_branch, class: 'ref-name') + end + end + private def cached_can_be_reverted? diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index a4a2c015c4e..2a916b13f52 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -10,4 +10,5 @@ class ClusterApplicationEntity < Grape::Entity expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :email, if: -> (e, _) { e.respond_to?(:email) } expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) } + expose :can_uninstall?, as: :can_uninstall end diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb index 973ae5ce5aa..d9a800791f2 100644 --- a/app/services/ci/stop_environments_service.rb +++ b/app/services/ci/stop_environments_service.rb @@ -9,12 +9,11 @@ module Ci return unless @ref.present? - environments.each do |environment| - next unless environment.stop_action_available? - next unless can?(current_user, :stop_environment, environment) + environments.each { |environment| stop(environment) } + end - environment.stop_with_action!(current_user) - end + def execute_for_merge_request(merge_request) + merge_request.environments.each { |environment| stop(environment) } end private @@ -24,5 +23,12 @@ module Ci .new(project, current_user, ref: @ref, recently_updated: true) .execute end + + def stop(environment) + return unless environment.stop_action_available? + return unless can?(current_user, :stop_environment, environment) + + environment.stop_with_action!(current_user) + end end end diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index c592d608b89..3c6803d24e6 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -37,7 +37,7 @@ module Clusters end def check_timeout - if timeouted? + if timed_out? begin app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") end @@ -51,8 +51,8 @@ module Clusters install_command.pod_name end - def timeouted? - Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT + def timed_out? + Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT end def remove_installation_pod diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb new file mode 100644 index 00000000000..8786d295d6a --- /dev/null +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class CheckUninstallProgressService < BaseHelmService + def execute + return unless app.uninstalling? + + case installation_phase + when Gitlab::Kubernetes::Pod::SUCCEEDED + on_success + when Gitlab::Kubernetes::Pod::FAILED + on_failed + else + check_timeout + end + rescue Kubeclient::HttpError => e + log_error(e) + + app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code }) + end + + private + + def on_success + app.destroy! + rescue StandardError => e + app.make_errored!(_('Application uninstalled but failed to destroy: %{error_message}') % { error_message: e.message }) + ensure + remove_installation_pod + end + + def on_failed + app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) + end + + def check_timeout + if timed_out? + app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) + else + WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id) + end + end + + def pod_name + app.uninstall_command.pod_name + end + + def timed_out? + Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT + end + + def remove_installation_pod + helm_api.delete_pod!(pod_name) + end + + def installation_phase + helm_api.status(pod_name) + end + end + end +end diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index ae36da7b3dd..f723c42c049 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -10,8 +10,8 @@ module Clusters end def builder - cluster.method("application_#{application_name}").call || - cluster.method("build_application_#{application_name}").call + cluster.public_send(:"application_#{application_name}") || # rubocop:disable GitlabSecurity/PublicSend + cluster.public_send(:"build_application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb new file mode 100644 index 00000000000..f3a4c4f754a --- /dev/null +++ b/app/services/clusters/applications/destroy_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class DestroyService < ::Clusters::Applications::BaseService + def execute(_request) + instantiate_application.tap do |application| + break unless application.can_uninstall? + + application.make_scheduled! + + Clusters::Applications::UninstallWorker.perform_async(application.name, application.id) + end + end + + private + + def builder + cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend + end + end + end +end diff --git a/app/services/clusters/applications/uninstall_service.rb b/app/services/clusters/applications/uninstall_service.rb new file mode 100644 index 00000000000..50c8d806c14 --- /dev/null +++ b/app/services/clusters/applications/uninstall_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UninstallService < BaseHelmService + def execute + return unless app.scheduled? + + app.make_uninstalling! + uninstall + end + + private + + def uninstall + helm_api.uninstall(app.uninstall_command) + + Clusters::Applications::WaitForUninstallAppWorker.perform_in( + Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_errored!('Failed to uninstall.') + end + end + end +end diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb index 5071c31839c..0fa937da865 100644 --- a/app/services/clusters/applications/update_service.rb +++ b/app/services/clusters/applications/update_service.rb @@ -10,7 +10,7 @@ module Clusters end def builder - cluster.method("application_#{application_name}").call + cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index a8478e3a904..9d371e234ee 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -73,13 +73,13 @@ module Git def push_data @push_data ||= Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - limited_commits, - event_message, + project: project, + user: current_user, + oldrev: params[:oldrev], + newrev: params[:newrev], + ref: params[:ref], + commits: limited_commits, + message: event_message, commits_count: commits_count, push_options: params[:push_options] || {} ) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index b8334a87f6d..a9dd26c02ad 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -24,6 +24,11 @@ module MergeRequests end end + def cleanup_environments(merge_request) + Ci::StopEnvironmentsService.new(merge_request.source_project, current_user) + .execute_for_merge_request(merge_request) + end + private def handle_wip_event(merge_request) diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index 04527bb9713..e77051bb1c9 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -17,6 +17,7 @@ module MergeRequests execute_hooks(merge_request, 'close') invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches + cleanup_environments(merge_request) end merge_request diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index f26e3bee06f..c13f7dd5088 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -18,6 +18,7 @@ module MergeRequests invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches delete_non_latest_diffs(merge_request) + cleanup_environments(merge_request) end private diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 642465551c9..073c14040ce 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -94,16 +94,13 @@ module Projects return unless project.lfs_enabled? - lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute + result = Projects::LfsPointers::LfsImportService.new(project).execute - lfs_objects_to_download.each do |lfs_download_object| - Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object) - .execute + if result[:status] == :error + # To avoid aborting the importing process, we silently fail + # if any exception raises. + Gitlab::AppLogger.error("The Lfs import process failed. #{result[:message]}") end - rescue => e - # Right now, to avoid aborting the importing process, we silently fail - # if any exception raises. - Rails.logger.error("The Lfs import process failed. #{e.message}") end def import_data diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb index a9570176e81..05974948505 100644 --- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb +++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb @@ -21,9 +21,9 @@ module Projects # This method accepts two parameters: # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size } # - # Returns a hash with the structure { lfs_file_oids => download_link } + # Returns an array of LfsDownloadObject def execute(oids) - return {} unless project&.lfs_enabled? && remote_uri && oids.present? + return [] unless project&.lfs_enabled? && remote_uri && oids.present? get_download_links(oids) end diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb index 9215fa0a7bf..2afcce7099b 100644 --- a/app/services/projects/lfs_pointers/lfs_import_service.rb +++ b/app/services/projects/lfs_pointers/lfs_import_service.rb @@ -1,95 +1,23 @@ # frozen_string_literal: true -# This service manages the whole worflow of discovering the Lfs files in a -# repository, linking them to the project and downloading (and linking) the non -# existent ones. +# This service is responsible of managing the retrieval of the lfs objects, +# and call the service LfsDownloadService, which performs the download +# for each of the retrieved lfs objects module Projects module LfsPointers class LfsImportService < BaseService - include Gitlab::Utils::StrongMemoize - - HEAD_REV = 'HEAD'.freeze - LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze - LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze - - LfsImportError = Class.new(StandardError) - def execute - return {} unless project&.lfs_enabled? + return success unless project&.lfs_enabled? - if external_lfs_endpoint? - # If the endpoint host is different from the import_url it means - # that the repo is using a third party service for storing the LFS files. - # In this case, we have to disable lfs in the project - disable_lfs! + lfs_objects_to_download = LfsObjectDownloadListService.new(project).execute - return {} + lfs_objects_to_download.each do |lfs_download_object| + LfsDownloadService.new(project, lfs_download_object).execute end - get_download_links - rescue LfsDownloadLinkListService::DownloadLinksError => e - raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}" - end - - private - - def external_lfs_endpoint? - lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host - end - - def disable_lfs! - project.update(lfs_enabled: false) - end - - # rubocop: disable CodeReuse/ActiveRecord - def get_download_links - existent_lfs = LfsListService.new(project).execute - linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys) - - # Retrieving those oids not linked and which we need to download - not_linked_lfs = existent_lfs.except(*linked_oids) - - LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs) - end - # rubocop: enable CodeReuse/ActiveRecord - - def lfsconfig_endpoint_uri - strong_memoize(:lfsconfig_endpoint_uri) do - # Retrieveing the blob data from the .lfsconfig file - data = project.repository.lfsconfig_for(HEAD_REV) - # Parsing the data to retrieve the url - parsed_data = data&.match(LFS_ENDPOINT_PATTERN) - - if parsed_data - URI.parse(parsed_data[1]).tap do |endpoint| - endpoint.user ||= import_uri.user - endpoint.password ||= import_uri.password - end - end - end - rescue URI::InvalidURIError - raise LfsImportError, 'Invalid URL in .lfsconfig file' - end - - def import_uri - @import_uri ||= URI.parse(project.import_url) - rescue URI::InvalidURIError - raise LfsImportError, 'Invalid project import URL' - end - - def current_endpoint_uri - (lfsconfig_endpoint_uri || default_endpoint_uri) - end - - # The import url must end with '.git' here we ensure it is - def default_endpoint_uri - @default_endpoint_uri ||= begin - import_uri.dup.tap do |uri| - path = uri.path.gsub(%r(/$), '') - path += '.git' unless path.ends_with?('.git') - uri.path = path + LFS_BATCH_API_ENDPOINT - end - end + success + rescue => e + error(e.message) end end end diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb index 8401f3d1d89..e3c956250f0 100644 --- a/app/services/projects/lfs_pointers/lfs_link_service.rb +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -6,9 +6,9 @@ module Projects class LfsLinkService < BaseService # Accept an array of oids to link # - # Returns a hash with the same structure with oids linked + # Returns an array with the oid of the existent lfs objects def execute(oids) - return {} unless project&.lfs_enabled? + return [] unless project&.lfs_enabled? # Search and link existing LFS Object link_existing_lfs_objects(oids) diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb new file mode 100644 index 00000000000..5ba0f50f2ff --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# This service manages the whole worflow of discovering the Lfs files in a +# repository, linking them to the project and downloading (and linking) the non +# existent ones. +module Projects + module LfsPointers + class LfsObjectDownloadListService < BaseService + include Gitlab::Utils::StrongMemoize + + HEAD_REV = 'HEAD'.freeze + LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze + LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze + + LfsObjectDownloadListError = Class.new(StandardError) + + def execute + return [] unless project&.lfs_enabled? + + if external_lfs_endpoint? + # If the endpoint host is different from the import_url it means + # that the repo is using a third party service for storing the LFS files. + # In this case, we have to disable lfs in the project + disable_lfs! + + return [] + end + + # Getting all Lfs pointers already in the database and linking them to the project + linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys) + # Retrieving those oids not present in the database which we need to download + missing_oids = lfs_pointers_in_repository.except(*linked_oids) # rubocop: disable CodeReuse/ActiveRecord + # Downloading the required information and gathering it inside a LfsDownloadObject for each oid + LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids) + rescue LfsDownloadLinkListService::DownloadLinksError => e + raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}" + end + + private + + def external_lfs_endpoint? + lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host + end + + def disable_lfs! + unless project.update(lfs_enabled: false) + raise LfsDownloadLinkListService::DownloadLinksError, "Invalid project state" + end + end + + # Retrieves all lfs pointers in the repository + def lfs_pointers_in_repository + @lfs_pointers_in_repository ||= LfsListService.new(project).execute + end + + def lfsconfig_endpoint_uri + strong_memoize(:lfsconfig_endpoint_uri) do + # Retrieveing the blob data from the .lfsconfig file + data = project.repository.lfsconfig_for(HEAD_REV) + # Parsing the data to retrieve the url + parsed_data = data&.match(LFS_ENDPOINT_PATTERN) + + if parsed_data + URI.parse(parsed_data[1]).tap do |endpoint| + endpoint.user ||= import_uri.user + endpoint.password ||= import_uri.password + end + end + end + rescue URI::InvalidURIError + raise LfsObjectDownloadListError, 'Invalid URL in .lfsconfig file' + end + + def import_uri + @import_uri ||= URI.parse(project.import_url) + rescue URI::InvalidURIError + raise LfsObjectDownloadListError, 'Invalid project import URL' + end + + def current_endpoint_uri + (lfsconfig_endpoint_uri || default_endpoint_uri) + end + + # The import url must end with '.git' here we ensure it is + def default_endpoint_uri + @default_endpoint_uri ||= begin + import_uri.dup.tap do |uri| + path = uri.path.gsub(%r(/$), '') + path += '.git' unless path.ends_with?('.git') + uri.path = path + LFS_BATCH_API_ENDPOINT + end + end + end + end + end +end diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index cab507946b4..4f6ae07be7d 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -41,12 +41,11 @@ module Tags def build_push_data(tag) Gitlab::DataBuilder::Push.build( - project, - current_user, - tag.dereferenced_target.sha, - Gitlab::Git::BLANK_SHA, - "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", - []) + project: project, + user: current_user, + oldrev: tag.dereferenced_target.sha, + newrev: Gitlab::Git::BLANK_SHA, + ref: "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}") end end end diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb index ebfb20132d0..4743e9b02ce 100644 --- a/app/services/todos/destroy/entity_leave_service.rb +++ b/app/services/todos/destroy/entity_leave_service.rb @@ -37,8 +37,8 @@ module Todos private def enqueue_private_features_worker - project_ids.each do |project_id| - TodosDestroyer::PrivateFeaturesWorker.perform_async(project_id, user.id) + projects.each do |project| + TodosDestroyer::PrivateFeaturesWorker.perform_async(project.id, user.id) end end @@ -62,9 +62,8 @@ module Todos end # rubocop: enable CodeReuse/ActiveRecord - override :project_ids # rubocop: disable CodeReuse/ActiveRecord - def project_ids + def projects condition = case entity when Project { id: entity.id } @@ -72,13 +71,13 @@ module Todos { namespace_id: non_member_groups } end - Project.where(condition).select(:id) + Project.where(condition) end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def non_authorized_projects - project_ids.where('id NOT IN (?)', user.authorized_projects.select(:id)) + projects.where('id NOT IN (?)', user.authorized_projects.select(:id)) end # rubocop: enable CodeReuse/ActiveRecord @@ -110,7 +109,7 @@ module Todos authorized_reporter_projects = user .authorized_projects(Gitlab::Access::REPORTER).select(:id) - Issue.where(project_id: project_ids, confidential: true) + Issue.where(project_id: projects, confidential: true) .where('project_id NOT IN(?)', authorized_reporter_projects) .where('author_id != ?', user.id) .where('id NOT IN (?)', assigned_ids) diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index 41b787515b5..006ebf09576 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -1,6 +1,13 @@ = form_for @application_setting, url: admin_application_settings_path(anchor: 'js-logging-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) + %p + %strong + NOTE: + These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml. + The specific client side DSN setting is already handled as a component from a Sentry perspective and will be removed. + In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production. + %fieldset .form-group .form-check diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index e38a16e7a1a..80d706ae3d3 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -4,7 +4,7 @@ - page_title _('Kubernetes Cluster') - manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project -- expanded = Rails.env.test? +- expanded = expanded_by_default? - status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2f635757902..0c8f86c2822 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,6 +1,6 @@ - breadcrumb_title "General Settings" - @content_class = "limit-container-width" unless fluid_layout -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') } diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index d0f5cd94002..d21496ee0aa 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title "CI / CD Settings" - page_title "CI / CD" -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 916f98a62d1..75e4dc46c9b 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,6 +1,7 @@ %div - if Gitlab::CurrentSettings.help_page_text.present? - = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text) + .prepend-top-default.md + = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text) %hr %h1 diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml index 888be4ee282..ed3c9890efd 100644 --- a/app/views/projects/cleanup/_show.html.haml +++ b/app/views/projects/cleanup/_show.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.no-animate#cleanup{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml index ff6a9d49a61..59efcde5825 100644 --- a/app/views/projects/default_branch/_show.html.haml +++ b/app/views/projects/default_branch/_show.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 24d665761cc..fcf27351a21 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c04530dc62c..c15b84d0aac 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title _("General Settings") - page_title _("General") - @content_class = "limit-container-width" unless fluid_layout -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.general-settings.no-animate.expanded#js-general-settings .settings-header diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 0d8d7123a01..9293aa1b309 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -39,6 +39,8 @@ - presented_labels_sorted_by_title(issue.labels, issue.project).each do |label| = link_to_label(label, css_class: 'label-link') + = render_if_exists "projects/issues/issue_weight", issue: issue + .issuable-meta %ul.controls - if issue.closed? diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml deleted file mode 100644 index 90838a75214..00000000000 --- a/app/views/projects/issues/_merge_requests_status.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- time_format = '%b %e, %Y %l:%M%P %Z%z' - -- if merge_request.merged? - - mr_status_date = merge_request.merged_at - - mr_status_title = _('Merged') - - mr_status_icon = 'merge' - - mr_status_class = 'merged' -- elsif merge_request.closed? - - mr_status_date = merge_request.closed_event&.created_at - - mr_status_title = _('Closed') - - mr_status_icon = 'issue-close' - - mr_status_class = 'closed' -- else - - mr_status_date = merge_request.created_at - - mr_status_title = mr_status_date ? _('Opened') : _('Open') - - mr_status_icon = 'issue-open-m' - - mr_status_class = 'open' - -- if mr_status_date - - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>" -- else - - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span></div>" - -%span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } } - = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}") diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index a1ec2c887c2..0cd00d3e708 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? - protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') %section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 95027634de2..d7e16dbd40c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -16,6 +16,7 @@ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } %p = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') + = render_if_exists 'projects/new_ci_cd_banner_external_repo' %p - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank' = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide } @@ -42,6 +43,7 @@ %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Import project %span.d-block.d-sm-none Import + = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab .tab-content.gitlab-tab-content .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' } @@ -68,6 +70,8 @@ %h4 No import options available %p Contact an administrator to enable options for importing your project. + = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab + .save-project-loader.d-none .center %h2 diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 539b184e5c2..63748d8d85f 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.qa-protected-branches-settings.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index 9a50a51e4be..b0c87ac8c17 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 548b7c06867..5e3e1076c2c 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -2,7 +2,7 @@ - page_title _("CI / CD Settings") - page_title _("CI / CD") -- expanded = Rails.env.test? +- expanded = expanded_by_default? - general_expanded = @project.errors.empty? ? expanded : true %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index d01bbbf269e..e4e85de93da 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -6,6 +6,7 @@ - cronjob:gitlab_usage_ping - cronjob:import_export_project_cleanup - cronjob:pages_domain_verification_cron +- cronjob:pages_domain_removal_cron - cronjob:pipeline_schedule - cronjob:prune_old_events - cronjob:remove_expired_group_links @@ -32,6 +33,8 @@ - gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_configure - gcp_cluster:cluster_project_configure +- gcp_cluster:clusters_applications_wait_for_uninstall_app +- gcp_cluster:clusters_applications_uninstall - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb new file mode 100644 index 00000000000..85e8ecc4ad5 --- /dev/null +++ b/app/workers/clusters/applications/uninstall_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UninstallWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::UninstallService.new(app).execute + end + end + end + end +end diff --git a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb new file mode 100644 index 00000000000..163c99d3c3c --- /dev/null +++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class WaitForUninstallAppWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + INTERVAL = 10.seconds + TIMEOUT = 20.minutes + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::CheckUninstallProgressService.new(app).execute + end + end + end + end +end diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb new file mode 100644 index 00000000000..3aca123e5ac --- /dev/null +++ b/app/workers/pages_domain_removal_cron_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class PagesDomainRemovalCronWorker + include ApplicationWorker + include CronjobQueue + + def perform + return unless Feature.enabled?(:remove_disabled_domains) + + PagesDomain.for_removal.find_each do |domain| + domain.destroy! + rescue => e + Raven.capture_exception(e) + end + end +end diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index 02a69ea3b54..8a9ee7808e4 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -3,20 +3,26 @@ class PipelineScheduleWorker include ApplicationWorker include CronjobQueue + include ::Gitlab::ExclusiveLeaseHelpers + + EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock' + LOCK_TIMEOUT = 50.minutes # rubocop: disable CodeReuse/ActiveRecord def perform - Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) - .preload(:owner, :project).find_each do |schedule| - - Ci::CreatePipelineService.new(schedule.project, - schedule.owner, - ref: schedule.ref) - .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule) - rescue => e - error(schedule, e) - ensure - schedule.schedule_next_run! + in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do + Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) + .preload(:owner, :project).find_each do |schedule| + + schedule.schedule_next_run! + + Ci::CreatePipelineService.new(schedule.project, + schedule.owner, + ref: schedule.ref) + .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule) + rescue => e + error(schedule, e) + end end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/changelogs/unreleased/55948-help-text-formatting-wiki.yml b/changelogs/unreleased/55948-help-text-formatting-wiki.yml new file mode 100644 index 00000000000..e1e0475a117 --- /dev/null +++ b/changelogs/unreleased/55948-help-text-formatting-wiki.yml @@ -0,0 +1,5 @@ +--- +title: Format extra help page text like wiki +merge_request: 26782 +author: Bastian Blank +type: fixed diff --git a/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml new file mode 100644 index 00000000000..32f0e023923 --- /dev/null +++ b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml @@ -0,0 +1,5 @@ +--- +title: Fix base domain help text update +merge_request: 27746 +author: +type: fixed diff --git a/changelogs/unreleased/bw-add-graphql-groups.yml b/changelogs/unreleased/bw-add-graphql-groups.yml new file mode 100644 index 00000000000..f72ee1cf2b7 --- /dev/null +++ b/changelogs/unreleased/bw-add-graphql-groups.yml @@ -0,0 +1,5 @@ +--- +title: Add initial GraphQL query for Groups +merge_request: 27492 +author: +type: added diff --git a/changelogs/unreleased/feat-sentry-environment.yml b/changelogs/unreleased/feat-sentry-environment.yml new file mode 100644 index 00000000000..44ea19375f8 --- /dev/null +++ b/changelogs/unreleased/feat-sentry-environment.yml @@ -0,0 +1,5 @@ +--- +title: Allow Sentry configuration to be passed on gitlab.yml +merge_request: 27091 +author: Roger Meier +type: added diff --git a/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml b/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml new file mode 100644 index 00000000000..c34bc6d8b52 --- /dev/null +++ b/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml @@ -0,0 +1,5 @@ +--- +title: Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent +merge_request: 27663 +author: +type: fixed diff --git a/changelogs/unreleased/fix-environment-on-stop-not-work.yml b/changelogs/unreleased/fix-environment-on-stop-not-work.yml new file mode 100644 index 00000000000..72e58b26c4d --- /dev/null +++ b/changelogs/unreleased/fix-environment-on-stop-not-work.yml @@ -0,0 +1,5 @@ +--- +title: "`on_stop` is not automatically triggered with pipelines for merge requests" +merge_request: 27618 +author: +type: fixed diff --git a/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml b/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml new file mode 100644 index 00000000000..8803f9b52a4 --- /dev/null +++ b/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml @@ -0,0 +1,6 @@ +--- +title: Fix pipelines for merge requests does not show pipeline page when source branch + is removed +merge_request: 27803 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-version-v1.36.0.yml b/changelogs/unreleased/gitaly-version-v1.36.0.yml new file mode 100644 index 00000000000..22fdca8da80 --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.36.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.36.0 +merge_request: 27831 +author: +type: changed diff --git a/changelogs/unreleased/lock-pipeline-schedule-worker.yml b/changelogs/unreleased/lock-pipeline-schedule-worker.yml new file mode 100644 index 00000000000..1b889f01620 --- /dev/null +++ b/changelogs/unreleased/lock-pipeline-schedule-worker.yml @@ -0,0 +1,5 @@ +--- +title: Prevent concurrent execution of PipelineScheduleWorker +merge_request: 27781 +author: +type: performance diff --git a/changelogs/unreleased/pl-upgrade-letter_opener_web.yml b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml new file mode 100644 index 00000000000..9891344215a --- /dev/null +++ b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade letter_opener_web to support Rails 5.1 +merge_request: 27829 +author: +type: fixed diff --git a/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml new file mode 100644 index 00000000000..20a4be8c9ad --- /dev/null +++ b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml @@ -0,0 +1,5 @@ +--- +title: 'Refactored Karma spec files to Jest' +merge_request: 27688 +author: Martin Hobert +type: other diff --git a/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml new file mode 100644 index 00000000000..9b208cbaa0e --- /dev/null +++ b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml @@ -0,0 +1,5 @@ +--- +title: Remove pages domains if they weren't verified for 1 week +merge_request: 26227 +author: +type: added diff --git a/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml new file mode 100644 index 00000000000..00f897ac4b1 --- /dev/null +++ b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml @@ -0,0 +1,5 @@ +--- +title: Disable method replacement in avatar loading +merge_request: 27866 +author: +type: performance diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index bdac5b2a6a1..06530194907 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -315,6 +315,13 @@ production: &base # path: shared/registry # issuer: gitlab-issuer + + ## Error Reporting and Logging with Sentry + sentry: + # enabled: false + # dsn: https://<key>@sentry.io/<project> + # environment: 'production' # e.g. development, staging, production + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 3c426cdb969..154de3bc1b0 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -136,6 +136,8 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80) Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' +# / is not a valid relative URL root +Settings.gitlab['relative_url_root'] = '' if Settings.gitlab['relative_url_root'] == '/' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" @@ -216,6 +218,14 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi Settings.registry['path'] = Settings.absolute(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry')) # +# Error Reporting and Logging with Sentry +# +Settings['sentry'] ||= Settingslogic.new({}) +Settings.sentry['enabled'] ||= false +Settings.sentry['dsn'] ||= nil +Settings.sentry['environment'] ||= nil + +# # Pages # Settings['pages'] ||= Settingslogic.new({}) @@ -338,6 +348,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *' Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker' +Settings.cron_jobs['pages_domain_removal_cron_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['pages_domain_removal_cron_worker']['cron'] ||= '47 0 * * *' +Settings.cron_jobs['pages_domain_removal_cron_worker']['job_class'] = 'PagesDomainRemovalCronWorker' + Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *' Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker' diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 680cfa6f0ed..e5589ce0ad1 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -14,6 +14,7 @@ def configure_sentry Raven.configure do |config| config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn config.release = Gitlab.revision + config.current_environment = Gitlab.config.sentry.environment.presence # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) diff --git a/config/routes.rb b/config/routes.rb index bbf00208545..f5957f43655 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,6 +103,7 @@ Rails.application.routes.draw do scope :applications do post '/:application', to: 'clusters/applications#create', as: :install_applications patch '/:application', to: 'clusters/applications#update', as: :update_applications + delete '/:application', to: 'clusters/applications#destroy', as: :uninstall_applications end get :cluster_status, format: :json diff --git a/config/routes/project.rb b/config/routes/project.rb index f7841bbe595..61eb136f65b 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -361,7 +361,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do post :toggle_subscription post :mark_as_spam post :move - get :referenced_merge_requests get :related_branches get :can_create_branch get :realtime_changes diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index e6820f49ee2..62e5526c02b 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -31,26 +31,54 @@ Please consider creating a merge request to for them. MARKDOWN -def spin(team, project, category, branch_name) +def spin_for_category(team, project, category, branch_name) rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16)) reviewers = team.select { |member| member.reviewer?(project, category) } traintainers = team.select { |member| member.traintainer?(project, category) } maintainers = team.select { |member| member.maintainer?(project, category) } - # TODO: filter out people who are currently not in the office - # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652 - # # TODO: take CODEOWNERS into account? # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653 # Make traintainers have triple the chance to be picked as a reviewer - reviewer = (reviewers + traintainers + traintainers).sample(random: rng) - maintainer = maintainers.sample(random: rng) + reviewer = spin_for_person(reviewers + traintainers + traintainers, random: rng) + maintainer = spin_for_person(maintainers, random: rng) "| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |" end +# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the +# selection will change on next spin +def spin_for_person(people, random:) + person = nil + people = people.dup + + people.size.times do + person = people.sample(random: random) + + break person unless out_of_office?(person) + + people -= [person] + end + + person +end + +def out_of_office?(person) + username = CGI.escape(person.username) + api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status" + response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty + + if response.code == 200 + response["message"]&.match(/OOO/i) + else + false # this is no worse than not checking for OOO + end +rescue + false +end + def build_list(items) list = items.map { |filename| "* `#{filename}`" }.join("\n") @@ -63,7 +91,7 @@ end changes = helper.changes_by_category -# Ignore any files that are known but uncategoried. Prompt for any unknown files +# Ignore any files that are known but uncategorized. Prompt for any unknown files changes.delete(:none) categories = changes.keys - [:unknown] @@ -92,7 +120,7 @@ if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_l project = helper.project_name unknown = changes.fetch(:unknown, []) - rows = categories.map { |category| spin(team, project, category, canonical_branch_name) } + rows = categories.map { |category| spin_for_category(team, project, category, canonical_branch_name) } markdown(MESSAGE) markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? diff --git a/doc/README.md b/doc/README.md index 14a1eeffda0..dd4909ce303 100644 --- a/doc/README.md +++ b/doc/README.md @@ -218,12 +218,12 @@ scales to run your tests faster. The following documentation relates to the DevOps **Verify** stage: -| Verify Topics | Description | -|:---------------------------------------------------|:-----------------------------------------------------------------------------| -| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. | -| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. | -| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. | -| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. | +| Verify Topics | Description | +|:---------------------------------------------------------|:-----------------------------------------------------------------------------| +| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. | +| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. | +| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. | +| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -288,7 +288,7 @@ The following documentation relates to the DevOps **Configure** stage: | [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. | | [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | | [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | -| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. | +| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. | | [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. | | [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. | @@ -418,7 +418,7 @@ We have the following documentation to rapidly uplift your GitLab knowledge: | Topic | Description | |:-----------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------| -| [GitLab Basics](gitlab-basics/README.md) | Start working on the command line and with GitLab. | +| [GitLab basics guides](gitlab-basics/README.md) | Start working on the command line and with GitLab. | | [GitLab Workflow](workflow/README.md) and [overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Enhance your workflow with the best of GitLab Workflow. | | [Get started with GitLab CI/CD](ci/quick_start/README.md) | Quickly implement GitLab CI/CD. | | [Auto DevOps](topics/autodevops/index.md) | Learn more about GitLab's Auto DevOps. | diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index ec48bf4940b..cf02bbd9c92 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -29,7 +29,11 @@ curl --data "value=100" --header "PRIVATE-TOKEN: <your_access_token>" https://gi ## Available queries -A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID. +A first iteration of a GraphQL API includes the following queries + +1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID. + +1. `group` : Only basic group information is currently supported. ## GraphiQL diff --git a/doc/ci/README.md b/doc/ci/README.md index 123a5e50f14..440a79c7782 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -5,71 +5,113 @@ description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Inte # GitLab Continuous Integration (GitLab CI/CD) -GitLab CI/CD is GitLab's built-in tool for software development using continuous methodology: +GitLab CI/CD is a tool built into GitLab for software development +through the [continuous methodologies](introduction/index.md#introduction-to-cicd-methodologies): -- Continuous integration (CI). -- Continuous delivery and deployment (CD). - -Within the [DevOps lifecycle](../README.md#the-entire-devops-lifecycle), GitLab CI/CD spans -the [Verify (CI)](../README.md#verify) and [Release (CD)](../README.md#release) stages. +- Continuous Integration (CI) +- Continuous Delivery (CD) +- Continuous Deployment (CD) ## Overview -CI/CD is a vast area, so GitLab provides documentation for all levels of expertise. Consult the following table to find the right documentation for you: - -| Level of expertise | Resource | -|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------| -| New to the concepts of CI and CD | For a high-level overview, read an [introduction to CI/CD with GitLab](introduction/index.md). | -| Familiar with GitLab CI/CD concepts | After getting familiar with GitLab CI/CD, let us walk you through a simple example in our [getting started guide](quick_start/README.md). | -| A GitLab CI/CD expert | Jump straight to our [`.gitlab.yml`](yaml/README.md) reference. | - -Familiarity with GitLab Runner is also useful because it is responsible for running the jobs in your -CI/CD pipeline. On GitLab.com, shared Runners are enabled by default so you won't need to set this up to get started. - -## CI/CD with Auto DevOps - -[Auto DevOps](../topics/autodevops/index.md) is the default minimum-configuration method for -implementing CI/CD. Auto DevOps: - -- Provides simplified setup and execution of CI/CD. -- Allows GitLab to automatically detect, build, test, deploy, and monitor your applications. - -## Manually configured CI/CD - -For complete control, you can manually configure GitLab CI/CD. - -### Configuration and Usage - -The following topics contain configuration and usage information for all features of GitLab CI/CD: - -| Topic | Description | -|:--------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------| -| [Creating and using CI/CD pipelines](pipelines.md) | Understand, visualize, create, and use CI/CD pipelines. | -| [CI/CD Variables](variables/README.md) | Configuring and using environment variables in pipelines. | -| [Where variables can be used](variables/where_variables_can_be_used.md) | Where and how CI/CD variables can be used. | -| [User](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions | User access levels for performing certain CI actions. | -| [Configuring GitLab Runners](runners/README.md) | Configuring [GitLab Runner](https://docs.gitlab.com/runner/). | -| [Environments and deployments](environments.md) | Deploy the output of jobs into environments for reviewing, staging, and production. | -| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | -| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Using the output of jobs. | -| [Cache dependencies in GitLab CI/CD](caching/index.md) | Speed up pipelines using caching. | -| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. | -| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. | -| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. | -| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. | -| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | -| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | -| [Optimizing GitLab for large repositories](large_repositories/index.md) | Useful tips on how to optimize GitLab and GitLab Runner for big repositories. | -| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. | -| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | - -### GitLab Pages - -GitLab CI/CD can be used to build and host static websites. For more information, see the -documentation on [GitLab Pages](../user/project/pages/index.md), -or dive right into the [CI/CD step-by-step guide for Pages](../user/project/pages/getting_started_part_four.md). - -### Examples +Continuous Integration works by pushing small code chunks to your +application's code base hosted in a Git repository, and, to every +push, run a pipeline of scripts to build, test, and validate the +code changes before merging them into the main branch. + +Continuous Delivery and Deployment consist of a step further CI, +deploying your application to production at every +push to the default branch of the repository. + +These methodologies allow you to catch bugs and errors early in +the development cycle, ensuring that all the code deployed to +production complies with the code standards you established for +your app. + +For a complete overview of these methodologies and GitLab CI/CD, +read the [Introduction to CI/CD with GitLab](introduction/index.md). + +## Getting started + +GitLab CI/CD is configured by a file called `.gitlab-ci.yml` placed +at the repository's root. The scripts set in this file are executed +by the [GitLab Runner](https://docs.gitlab.com/runner/). + +To get started with GitLab CI/CD, we recommend you read through +the following documents: + +- [How GitLab CI/CD works](introduction/index.md#how-gitlab-cicd-works). +- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow). +- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md). + +You can also get started by using one of the +[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates) +available through the UI. You can use them by creating a new file, +choosing a template that suits your application, and adjusting it +to your needs: + +![Use a .gitlab-ci.yml template](img/add_file_template_11_10.png) + +For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide. + +Once you're familiar with how GitLab CI/CD works, see the +[`. gitlab-ci.yml` full reference](yaml/README.md) +for all the attributes you can set and use. + +NOTE: **Note:** +GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#extra-shared-runners-pipeline-minutes-quota). + +## GitLab CI/CD configuration + +GitLab CI/CD supports numerous configuration options: + +| Configuration | Description | +|:--- |:--- | +| [Pipelines](pipelines.md) | Structure your CI/CD process through pipelines. | +| [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. | +| [Environments](environments.md) | Deploy your application to different environments (e.g., staging, production). | +| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. | +| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. | +| [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. | +| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. | +| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules. | +| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. | +| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. | +| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. | +| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. | +| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. | +| [`.gitlab-ci.yml` full reference](yaml/README.md) | All the attributes you can use with GitLab CI/CD. | + +Note that certain operations can only be performed according to the +[user](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions. + +## GitLab CI/CD feature set + +You can also use the vast GitLab CI/CD feature set to easily configure +it for specific purposes: + +| Feature | Description | +|:--- |:--- | +| [Auto Deploy](../topics/autodevops/index.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. | +| [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. | +| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. | +| [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) **[PREMIUM]** | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. | +| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | +| [CI services](services/README.md)| Link Docker containers with your base image. | +| [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]**| Check your Docker containers for known vulnerabilities. | +| [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]**| Analyze your dependencies for known vulnerabilities. | +| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. | +| [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html) **[PREMIUM]** | Deploy your features behind Feature Flags. | +| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | +| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. | +| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. | +| [Interactive Web Terminals](interactive_web_terminal/index.md) **[CORE ONLY]** | Open an interactive web terminal to debug the running jobs. | +| [JUnit tests](junit_test_reports.md)| Identify script failures directly on merge requests. | +| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | +| [Security Test reports](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate) **[ULTIMATE]** | Check for app vulnerabilities. | +| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. | + +## GitLab CI/CD examples GitLab provides examples of configuring GitLab CI/CD in the form of: @@ -78,9 +120,10 @@ GitLab provides examples of configuring GitLab CI/CD in the form of: - [`multi-project-pipelines`](https://gitlab.com/gitlab-examples/multi-project-pipelines) for examples of implementing multi-project pipelines. - [`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps. -### Administration +## GitLab CI/CD administration **[CORE ONLY]** -As a GitLab administrator, you can change the default behavior of GitLab CI/CD for: +As a GitLab administrator, you can change the default behavior +of GitLab CI/CD for: - An [entire GitLab instance](../user/admin_area/settings/continuous_integration.md). - Specific projects, using [pipelines settings](../user/project/pipelines/settings.md). @@ -90,33 +133,22 @@ See also: - [How to enable or disable GitLab CI/CD](enable_or_disable_ci.md). - Other [CI administration settings](../administration/index.md#continuous-integration-settings). -### Using Docker - -Docker is commonly used with GitLab CI/CD. Learn more about how to to accomplish this with the following -documentation: - -| Topic | Description | -|:-------------------------------------------------------------------------|:-------------------------------------------------------------------------| -| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. | -| [Building Docker images with GitLab CI/CD](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. | - -Related topics include: - -- [Docker integration](docker/README.md). -- [CI services (linked Docker containers)](services/README.md). +## References -## Why GitLab CI/CD? +### Why GitLab CI/CD? -The following articles explain reasons to use GitLab CI/CD for your CI/CD infrastructure: +The following articles explain reasons to use GitLab CI/CD +for your CI/CD infrastructure: - [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/) - [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/) See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation. -## Breaking changes +### Breaking changes -As GitLab CI/CD has evolved, certain breaking changes have been necessary. These are: +As GitLab CI/CD has evolved, certain breaking changes have +been necessary. These are: - [CI variables renaming for GitLab 9.0](variables/deprecated_variables.md#gitlab-90-renamed-variables). Read about the deprecated CI variables and what you should use for GitLab 9.0+. diff --git a/doc/ci/img/add_file_template_11_10.png b/doc/ci/img/add_file_template_11_10.png Binary files differnew file mode 100644 index 00000000000..ca04d72615b --- /dev/null +++ b/doc/ci/img/add_file_template_11_10.png diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 4e2213f7742..c4e5995714d 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -23,11 +23,6 @@ one of the [Merge request coaches][team]. If you need assistance with security scans or comments, feel free to include the Security Team (`@gitlab-com/gl-security`) in the review. -The `danger-review` CI job will randomly pick a reviewer and a maintainer for -each area of the codebase that your merge request seems to touch. It only makes -recommendations - feel free to override it if you think someone else is a better -fit! - Depending on the areas your merge request touches, it must be **approved** by one or more [maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer): @@ -37,6 +32,26 @@ widget. Reviewers can add their approval by [approving additionally](https://doc Getting your merge request **merged** also requires a maintainer. If it requires more than one approval, the last maintainer to review and approve it will also merge it. +### Reviewer roulette + +The `danger-review` CI job will randomly pick a reviewer and a maintainer for +each area of the codebase that your merge request seems to touch. It only makes +recommendations - feel free to override it if you think someone else is a better +fit! + +It picks reviewers and maintainers from the list at the +[engineering projects](https://about.gitlab.com/handbook/engineering/projects/) +page, with these behaviours: + +1. It will not pick people whose [GitLab status](../user/profile/#current-status) + contains the string 'OOO'. +2. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) + are three times as likely to be picked as other reviewers. +3. It always picks the same reviewers and maintainers for the same + branch name (unless their OOO status changes, as in point 1). It + removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so + that it can be stable for backport branches. + ### Approval guidelines As described in the section on the responsibility of the maintainer below, you diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index 4e15f7cfd49..aa008d6f768 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -2,18 +2,34 @@ comments: false --- -# GitLab basics - -Step-by-step guides on the basics of working with Git and GitLab. - -- [Command line basics](command-line-commands.md) -- [Start using Git on the command line](start-using-git.md) -- [Create and add your SSH Keys](create-your-ssh-keys.md) -- [Create a project](create-project.md) -- [Create a group](../user/group/index.md#create-a-new-group) -- [Create a branch](create-branch.md) -- [Fork a project](fork-project.md) -- [Add a file](add-file.md) -- [Add an image](add-image.md) -- [Create an issue](../user/project/issues/create_new_issue.md) -- [Create a merge request](add-merge-request.md) +# GitLab basics guides + +This section provides resources to help you start with GitLab by focusing on basic functionality. + +This documentation is split into the following groups: + +- [GitLab-specific functionality](#gitlab-basics), for basic GitLab features. +- [General Git functionality](#git-basics), for working with Git in conjunction with GitLab. + +## GitLab basics + +The following are guides to basic GitLab functionality: + +- [Create and add your SSH Keys](create-your-ssh-keys.md), for enabling Git over SSH. +- [Create a project](create-project.md), to start using GitLab. +- [Create a group](../user/group/index.md#create-a-new-group), to combine and administer projects together. +- [Create a branch](create-branch.md), to make changes to files stored in a project's repository. +- [Fork a project](fork-project.md), to duplicate projects so they can be worked on in parallel. +- [Add a file](add-file.md), to add new files to a project's repository. +- [Add an image](add-image.md), to add new images to a project's repository. +- [Create an issue](../user/project/issues/create_new_issue.md), to start collaborating within a project. +- [Create a merge request](add-merge-request.md), to request changes made in a branch be merged into a project's repository. + +## Git basics + +If you're unfamiliar with the command line, these resources will help: + +- [Command line basics](command-line-commands.md), for those unfamiliar with the command line interface. +- [Start using Git on the command line](start-using-git.md), for some simple Git commands. + +More Git resources are available at GitLab's [Git documentation](../topics/git/index.md). diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index 5e67cb0ef16..f924ff8dfde 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -16,7 +16,7 @@ The Admin Area is made up of the following sections: | Section | Description | |:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------| -| Overview | View your GitLab [Dashboard](#admin-dashboard), and maintain projects, users, groups, jobs, runners, and Gitaly servers. | +| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), users, groups, jobs, runners, and Gitaly servers. | | Monitoring | View GitLab system information, and information on background jobs, logs, [health checks](monitoring/health_check.md), request profiles, and audit logs. | | Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. | | System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. | @@ -46,4 +46,28 @@ The Dashboard is the default view of the Admin Area, and is made up of the follo | Groups | The total number of groups, up to 10 of the latest groups, and the option of creating a new group. | | Statistics | Totals of all elements of the GitLab instance. | | Features | All features available on the GitLab instance. Enabled features are marked with a green circle icon, and disabled features are marked with a power icon. | -| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. |
\ No newline at end of file +| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. | + +## Administer Projects + +You can administer all projects in the GitLab instance from the Admin Area's Projects page. + +To access the Projects page, go to **Admin Area > Overview > Projects**. + +Click the **All**, **Private**, **Internal**, or **Public** tab to list only projects of that +criteria. + +By default, all projects are listed, in reverse order of when they were last updated. For each +project, the name, namespace, description, and size is listed, also options to **Edit** or +**Delete** it. + +Sort projects by **Name**, **Last created**, **Oldest created**, **Last updated**, **Oldest +updated**, **Owner**, and choose to hide or show archived projects. + +In the **Filter by name** field, type the project name you want to find, and GitLab will filter +them as you type. + +Select from the **Namespace** dropdown to filter only projects in that namespace. + +You can combine the filter options. For example, click the **Public** tab, and enter `score` in +the **Filter by name...** input box to list only public projects with `score` in their name.
\ No newline at end of file diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 6fc083170b6..23f1ce7a15a 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -7,9 +7,9 @@ in the table below. | Field | Description | | ----- | ----------- | -| `title` | A title for the issue tracker (to differentiate between instances, for example) | +| `title` | A title for the issue tracker (to differentiate between instances, for example). | | `description` | A name for the issue tracker (to differentiate between instances, for example) | -| `project_url` | Currently unused. Will be changed in a future release. | +| `project_url` | The URL to the project in the custom issue tracker. | | `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. | | `new_issue_url` | Currently unused. Will be changed in a future release. | diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index ad47b848bea..31020de5208 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -151,7 +151,7 @@ Create lists for each of your team members and quickly drag-and-drop issues onto ## Permissions -[Developers and up](../permissions.md) can use all the functionality of the +[Reporters and up](../permissions.md) can use all the functionality of the Issue Board, that is, create or delete lists and drag issues from one list to another. ## GitLab Enterprise features for Issue Boards diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index d347f3c13a4..68890aa8e30 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -99,15 +99,15 @@ module Gitlab end CATEGORY_LABELS = { - docs: "~Documentation", + docs: "~Documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now. none: "", qa: "~QA" }.freeze # rubocop:disable Style/RegexpLiteral CATEGORIES = { - %r{\Adoc/} => :docs, - %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs, + %r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`. + %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :none, # To reinstate roulette for documentation, set to `:docs`. %r{\A(ee/)?app/(assets|views)/} => :frontend, %r{\A(ee/)?public/} => :frontend, @@ -148,7 +148,7 @@ module Gitlab # Fallbacks in case the above patterns miss anything %r{\.rb\z} => :backend, - %r{\.(md|txt)\z} => :docs, + %r{\.(md|txt)\z} => :none, # To reinstate roulette for documentation, set to `:docs`. %r{\.js\z} => :frontend }.freeze # rubocop:enable Style/RegexpLiteral diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index af385d7d4ca..40bda3410e1 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -58,7 +58,10 @@ module Gitlab # } # # rubocop:disable Metrics/ParameterLists - def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {}) + def build( + project:, user:, ref:, oldrev: nil, newrev: nil, + commits: [], commits_count: nil, message: nil, push_options: {}) + commits = Array(commits) # Total commits count @@ -113,7 +116,12 @@ module Gitlab ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" commits = project.repository.commits(project.default_branch.to_s, limit: 3) - build(project, user, commits.last&.id, commits.first&.id, ref, commits) + build(project: project, + user: user, + oldrev: commits.last&.id, + newrev: commits.first&.id, + ref: ref, + commits: commits) end def sample_data diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb index 8eb3c28ab70..d0577d7a4ff 100644 --- a/lib/gitlab/git/object_pool.rb +++ b/lib/gitlab/git/object_pool.rb @@ -40,6 +40,10 @@ module Gitlab @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY, gl_project_path) end + def fetch + object_pool_service.fetch(source_repository) + end + private def object_pool_service diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb index ce1fb4d68ae..d7fac26bc13 100644 --- a/lib/gitlab/gitaly_client/object_pool_service.rb +++ b/lib/gitlab/gitaly_client/object_pool_service.rb @@ -33,6 +33,15 @@ module Gitlab GitalyClient.call(storage, :object_pool_service, :link_repository_to_object_pool, request, timeout: GitalyClient.fast_timeout) end + + def fetch(repository) + request = Gitaly::FetchIntoObjectPoolRequest.new( + object_pool: object_pool, + origin: repository.gitaly_repository + ) + + GitalyClient.call(storage, :object_pool_service, :fetch_into_object_pool, request) + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index e00309e7946..582c3065189 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -15,7 +15,12 @@ module Gitlab gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled + + if Gitlab::CurrentSettings.clientside_sentry_enabled + gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn + gon.sentry_environment = Gitlab.config.sentry.environment + end + gon.gitlab_url = Gitlab.config.gitlab.url gon.revision = Gitlab.revision gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index 7dfd9ed4f35..ff1dadf9247 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -22,6 +22,13 @@ module Gitlab alias_method :update, :install + def uninstall(command) + namespace.ensure_exists! + + delete_pod!(command.pod_name) + kubeclient.create_pod(command.pod_resource) + end + ## # Returns Pod phase # diff --git a/lib/gitlab/prometheus/query_variables.rb b/lib/gitlab/prometheus/query_variables.rb index 1cc85d4b4a6..dca09aef47d 100644 --- a/lib/gitlab/prometheus/query_variables.rb +++ b/lib/gitlab/prometheus/query_variables.rb @@ -4,9 +4,13 @@ module Gitlab module Prometheus module QueryVariables def self.call(environment) + deployment_platform = environment.deployment_platform + namespace = deployment_platform&.namespace_for(environment.project) || + deployment_platform&.actual_namespace || '' + { ci_environment_slug: environment.slug, - kube_namespace: environment.deployment_platform&.actual_namespace || '', + kube_namespace: namespace, environment_filter: %{container_name!="POD",environment="#{environment.slug}"} } end diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index fb303e3fb0c..c102fa14cfc 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -7,7 +7,7 @@ module Gitlab module SidekiqConfig QUEUE_CONFIG_PATHS = %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml].freeze - # This method is called by `bin/sidekiq-cluster` in EE, which runs outside + # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside # of bundler/Rails context, so we cannot use any gem or Rails methods. def self.worker_queues(rails_path = Rails.root.to_s) @worker_queues ||= {} @@ -19,7 +19,7 @@ module Gitlab end end - # This method is called by `bin/sidekiq-cluster` in EE, which runs outside + # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside # of bundler/Rails context, so we cannot use any gem or Rails methods. def self.expand_queues(queues, all_queues = self.worker_queues) return [] if queues.empty? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0a5cd6730e6..7b5770a6075 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -942,6 +942,9 @@ msgstr "" msgid "Application settings saved successfully" msgstr "" +msgid "Application uninstalled but failed to destroy: %{error_message}" +msgstr "" + msgid "Application was successfully destroyed." msgstr "" @@ -1293,6 +1296,9 @@ msgstr "" msgid "Badges|e.g. %{exampleUrl}" msgstr "" +msgid "Balsamiq file could not be loaded." +msgstr "" + msgid "BambooService|A continuous integration and build server" msgstr "" @@ -1611,6 +1617,9 @@ msgstr "" msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded." msgstr "" +msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above." +msgstr "" + msgid "Cannot skip two factor authentication setup" msgstr "" @@ -2711,6 +2720,9 @@ msgstr "" msgid "Copy secret to clipboard" msgstr "" +msgid "Copy source to clipboard" +msgstr "" + msgid "Copy to clipboard" msgstr "" @@ -3289,6 +3301,9 @@ msgstr "" msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?" msgstr "" +msgid "Dockerfile" +msgstr "" + msgid "Domain" msgstr "" @@ -3706,6 +3721,9 @@ msgstr "" msgid "Error loading branches." msgstr "" +msgid "Error loading file viewer." +msgstr "" + msgid "Error loading last commit." msgstr "" @@ -3724,6 +3742,9 @@ msgstr "" msgid "Error loading template." msgstr "" +msgid "Error loading viewer" +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" @@ -3757,9 +3778,15 @@ msgstr "" msgid "Error uploading file" msgstr "" +msgid "Error uploading file: %{stripped}" +msgstr "" + msgid "Error while loading the merge request. Please try again." msgstr "" +msgid "Error while loading the project data. Please try again." +msgstr "" + msgid "Error while migrating %{upload_id}: %{error_message}" msgstr "" @@ -3958,6 +3985,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to connect to the prometheus server" +msgstr "" + msgid "Failed to create repository via gitlab-shell" msgstr "" @@ -5189,6 +5219,9 @@ msgstr "" msgid "LFSStatus|Enabled" msgstr "" +msgid "LICENSE" +msgstr "" + msgid "Label" msgstr "" @@ -6271,6 +6304,12 @@ msgstr "" msgid "Opens in a new window" msgstr "" +msgid "Operation failed. Check pod logs for %{pod_name} for more details." +msgstr "" + +msgid "Operation timed out. Check pod logs for %{pod_name} for more details." +msgstr "" + msgid "Operations" msgstr "" @@ -6646,6 +6685,9 @@ msgstr "" msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access." msgstr "" +msgid "Please select a file" +msgstr "" + msgid "Please select a group." msgstr "" @@ -6688,6 +6730,9 @@ msgstr "" msgid "Preview" msgstr "" +msgid "Preview Markdown" +msgstr "" + msgid "Preview changes" msgstr "" @@ -7183,6 +7228,57 @@ msgstr "" msgid "ProjectSettings|When conflicts arise the user is given the option to rebase" msgstr "" +msgid "ProjectTemplates|.NET Core" +msgstr "" + +msgid "ProjectTemplates|Android" +msgstr "" + +msgid "ProjectTemplates|Go Micro" +msgstr "" + +msgid "ProjectTemplates|Netlify/GitBook" +msgstr "" + +msgid "ProjectTemplates|Netlify/Hexo" +msgstr "" + +msgid "ProjectTemplates|Netlify/Hugo" +msgstr "" + +msgid "ProjectTemplates|Netlify/Jekyll" +msgstr "" + +msgid "ProjectTemplates|Netlify/Plain HTML" +msgstr "" + +msgid "ProjectTemplates|NodeJS Express" +msgstr "" + +msgid "ProjectTemplates|Pages/GitBook" +msgstr "" + +msgid "ProjectTemplates|Pages/Hexo" +msgstr "" + +msgid "ProjectTemplates|Pages/Hugo" +msgstr "" + +msgid "ProjectTemplates|Pages/Jekyll" +msgstr "" + +msgid "ProjectTemplates|Pages/Plain HTML" +msgstr "" + +msgid "ProjectTemplates|Ruby on Rails" +msgstr "" + +msgid "ProjectTemplates|Spring" +msgstr "" + +msgid "ProjectTemplates|iOS (Swift)" +msgstr "" + msgid "Projects" msgstr "" @@ -8710,6 +8806,9 @@ msgstr "" msgid "Switch to GitLab Next" msgstr "" +msgid "Switch to the source to copy it to the clipboard" +msgstr "" + msgid "System Hooks" msgstr "" @@ -9132,6 +9231,9 @@ msgstr "" msgid "There is already a repository with that name on disk" msgstr "" +msgid "There was a problem communicating with your device." +msgstr "" + msgid "There was an error loading users activity calendar." msgstr "" @@ -9156,6 +9258,9 @@ msgstr "" msgid "There was an error when unsubscribing from this label." msgstr "" +msgid "There was an error while fetching cycle analytics data." +msgstr "" + msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." msgstr "" @@ -9207,6 +9312,12 @@ msgstr "" msgid "This container registry has been scheduled for deletion." msgstr "" +msgid "This device has already been registered with us." +msgstr "" + +msgid "This device has not been registered with us." +msgstr "" + msgid "This diff is collapsed." msgstr "" @@ -9793,12 +9904,18 @@ msgstr "" msgid "Type" msgstr "" +msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." +msgstr "" + msgid "Unable to connect to server: %{error}" msgstr "" msgid "Unable to load the diff. %{button_try_again}" msgstr "" +msgid "Unable to regenerate public ssh key." +msgstr "" + msgid "Unable to schedule a pipeline to run immediately" msgstr "" @@ -10219,6 +10336,9 @@ msgstr "" msgid "VisibilityLevel|Unknown" msgstr "" +msgid "Wait for the source to load to copy it to the clipboard" +msgstr "" + msgid "Want to see the data? Please ask an administrator for access." msgstr "" diff --git a/package.json b/package.json index d2ee29fe06d..6fd364251e8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.59.0", - "@gitlab/ui": "^3.5.0", + "@gitlab/ui": "^3.7.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-upload-client": "^10.0.0", @@ -90,7 +90,7 @@ "jszip-utils": "^0.0.2", "katex": "^0.10.0", "marked": "^0.3.12", - "mermaid": "^8.0.0-rc.8", + "mermaid": "^8.0.0", "monaco-editor": "^0.15.6", "monaco-editor-webpack-plugin": "^1.7.0", "mousetrap": "^1.4.6", diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 309ae6cd986..e689ba4c69c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -3,11 +3,6 @@ module QA context 'Create' do describe 'Wiki management' do - def validate_content(content) - expect(page).to have_content('Wiki was successfully updated') - expect(page).to have_content(/#{content}/) - end - it 'user creates, edits, clones, and pushes to the wiki' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) @@ -38,6 +33,11 @@ module QA expect(page).to have_content('My Third Wiki Content') end + + def validate_content(content) + expect(page).to have_content('Wiki was successfully updated') + expect(page).to have_content(/#{content}/) + end end end end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 8be22dc0278..9455e462617 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -216,6 +216,7 @@ HELM_CMD=$(cat << EOF --set global.ingress.configureCertmanager=false \ --set global.ingress.tls.secretName=tls-cert \ --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" + --set nginx-ingress.controller.service.enableHttp=false \ --set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \ --set nginx-ingress.controller.resources.requests.memory=440M \ --set nginx-ingress.controller.replicaCount=2 \ diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb index cd1a01f8acc..70b34f071c8 100644 --- a/spec/controllers/projects/clusters/applications_controller_spec.rb +++ b/spec/controllers/projects/clusters/applications_controller_spec.rb @@ -145,4 +145,66 @@ describe Projects::Clusters::ApplicationsController do it_behaves_like 'a secure endpoint' end end + + describe 'DELETE destroy' do + subject do + delete :destroy, params: params.merge(namespace_id: project.namespace, project_id: project) + end + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id } } + let(:worker_class) { Clusters::Applications::UninstallWorker } + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(worker_class).to receive(:perform_async).with(application.name, application.id).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_prometheus).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(worker_class).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index fe56ac5b71d..d78f01828d7 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -6,6 +6,11 @@ FactoryBot.define do status(-2) end + trait :errored do + status(-1) + status_reason 'something went wrong' + end + trait :installable do status 0 end @@ -30,17 +35,21 @@ FactoryBot.define do status 5 end - trait :errored do - status(-1) + trait :update_errored do + status(6) status_reason 'something went wrong' end - trait :update_errored do - status(6) + trait :uninstalling do + status 7 + end + + trait :uninstall_errored do + status(8) status_reason 'something went wrong' end - trait :timeouted do + trait :timed_out do installing updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago } end diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index b74f72f2bd3..db8384877b0 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -45,6 +45,10 @@ nNp/xedE1YxutQ== remove_at { 1.day.from_now } end + trait :should_be_removed do + remove_at { 1.day.ago } + end + trait :unverified do verified_at nil end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index cf334e1e4da..4ec44cb05b3 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -331,11 +331,9 @@ describe 'Pipeline', :js do merge_request.all_pipelines.last end - before do + it 'shows the pipeline information' do visit_pipeline - end - it 'shows the pipeline information' do within '.pipeline-info' do expect(page).to have_content("#{pipeline.statuses.count} jobs " \ "for !#{merge_request.iid} " \ @@ -347,6 +345,21 @@ describe 'Pipeline', :js do end end + context 'when source branch does not exist' do + before do + project.repository.rm_branch(user, merge_request.source_branch) + end + + it 'does not link to the source branch commit path' do + visit_pipeline + + within '.pipeline-info' do + expect(page).not_to have_link(merge_request.source_branch) + expect(page).to have_content(merge_request.source_branch) + end + end + end + context 'when source project is a forked project' do let(:source_project) { fork_project(project, user, repository: true) } @@ -386,11 +399,11 @@ describe 'Pipeline', :js do before do pipeline.update(user: user) - - visit_pipeline end it 'shows the pipeline information' do + visit_pipeline + within '.pipeline-info' do expect(page).to have_content("#{pipeline.statuses.count} jobs " \ "for !#{merge_request.iid} " \ @@ -405,6 +418,21 @@ describe 'Pipeline', :js do end end + context 'when target branch does not exist' do + before do + project.repository.rm_branch(user, merge_request.target_branch) + end + + it 'does not link to the target branch commit path' do + visit_pipeline + + within '.pipeline-info' do + expect(page).not_to have_link(merge_request.target_branch) + expect(page).to have_content(merge_request.target_branch) + end + end + end + context 'when source project is a forked project' do let(:source_project) { fork_project(project, user, repository: true) } diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 9da07a0b253..695175689b9 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -36,7 +36,8 @@ "external_hostname": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] }, "email": { "type": ["string", "null"] }, - "update_available": { "type": ["boolean", "null"] } + "update_available": { "type": ["boolean", "null"] }, + "can_uninstall": { "type": "boolean" } }, "required" : [ "name", "status" ] } diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index 5103cb4f69f..a61103397eb 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -6,7 +6,7 @@ import { loadHTMLFixture } from 'helpers/fixtures'; import { setTestTimeout } from 'helpers/timeout'; import $ from 'jquery'; -const { INSTALLING, INSTALLABLE, INSTALLED, NOT_INSTALLABLE } = APPLICATION_STATUS; +const { INSTALLING, INSTALLABLE, INSTALLED } = APPLICATION_STATUS; describe('Clusters', () => { setTestTimeout(1000); @@ -317,13 +317,12 @@ describe('Clusters', () => { let ingressNewState; beforeEach(() => { - ingressPreviousState = { status: INSTALLABLE }; - ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' }; + ingressPreviousState = { externalIp: null }; + ingressNewState = { externalIp: '127.0.0.1' }; }); - describe(`when ingress application new status is ${INSTALLED}`, () => { + describe(`when ingress have an external ip assigned`, () => { beforeEach(() => { - ingressNewState.status = INSTALLED; cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); }); @@ -338,31 +337,11 @@ describe('Clusters', () => { }); }); - describe(`when ingress application new status is different from ${INSTALLED}`, () => { + describe(`when ingress does not have an external ip assigned`, () => { it('hides custom domain help text', () => { - ingressNewState.status = NOT_INSTALLABLE; - cluster.ingressDomainHelpText.classList.remove('hide'); - - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); - }); - }); - - describe('when ingress application new status and old status are the same', () => { - it('does not display custom domain help text', () => { - ingressPreviousState.status = INSTALLED; - ingressNewState.status = ingressPreviousState.status; - - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); - }); - }); - - describe(`when ingress new status is ${INSTALLED} and there isn’t an ip assigned`, () => { - it('does not display custom domain help text', () => { + ingressPreviousState.externalIp = '127.0.0.1'; ingressNewState.externalIp = null; + cluster.ingressDomainHelpText.classList.remove('hide'); cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js index 6bff1521695..691ebe43d6b 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('collapsedCalendarIcon', () => { let vm; @@ -26,7 +26,7 @@ describe('collapsedCalendarIcon', () => { }); it('should emit click event when container is clicked', () => { - const click = jasmine.createSpy(); + const click = jest.fn(); vm.$on('click', click); vm.$el.click(); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index c507a97d37e..062ebfa01c9 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('collapsedGroupedDatePicker', () => { let vm; @@ -13,7 +13,7 @@ describe('collapsedGroupedDatePicker', () => { describe('toggleCollapse events', () => { beforeEach(done => { - spyOn(vm, 'toggleSidebar'); + jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {}); vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js index 805ba7b9947..5e2bca6efc9 100644 --- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('sidebarDatePicker', () => { let vm; @@ -13,7 +13,7 @@ describe('sidebarDatePicker', () => { }); it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { - const toggleCollapse = jasmine.createSpy(); + const toggleCollapse = jest.fn(); vm.$on('toggleCollapse', toggleCollapse); vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click(); @@ -90,7 +90,7 @@ describe('sidebarDatePicker', () => { }); it('should emit saveDate when remove button is clicked', () => { - const saveDate = jasmine.createSpy(); + const saveDate = jest.fn(); vm.$on('saveDate', saveDate); vm.$el.querySelector('.value-content .btn-blank').click(); @@ -110,7 +110,7 @@ describe('sidebarDatePicker', () => { }); it('should emit toggleCollapse when toggle sidebar is clicked', () => { - const toggleCollapse = jasmine.createSpy(); + const toggleCollapse = jest.fn(); vm.$on('toggleCollapse', toggleCollapse); vm.$el.querySelector('.title .gutter-toggle').click(); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js index c44b04009ca..6aee616c324 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js @@ -3,25 +3,35 @@ import Vue from 'vue'; import LabelsSelect from '~/labels_select'; import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig, mockLabels } from './mock_data'; +import { mount } from '@vue/test-utils'; +import { + mockConfig, + mockLabels, +} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = (config = mockConfig) => { const Component = Vue.extend(baseComponent); - return mountComponent(Component, config); + return mount(Component, { + propsData: config, + sync: false, + }); }; describe('BaseComponent', () => { + let wrapper; let vm; - beforeEach(() => { - vm = createComponent(); + beforeEach(done => { + wrapper = createComponent(); + + ({ vm } = wrapper); + + Vue.nextTick(done); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('computed', () => { @@ -31,11 +41,9 @@ describe('BaseComponent', () => { }); it('returns correct string when showCreate prop is `false`', () => { - const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false }); - const vmNonEditable = createComponent(mockConfigNonEditable); + wrapper.setProps({ showCreate: false }); - expect(vmNonEditable.hiddenInputName).toBe('label_id[]'); - vmNonEditable.$destroy(); + expect(vm.hiddenInputName).toBe('label_id[]'); }); }); @@ -45,11 +53,9 @@ describe('BaseComponent', () => { }); it('return `Create group label` when `isProject` prop is false', () => { - const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false }); - const vmGroup = createComponent(mockConfigGroup); + wrapper.setProps({ isProject: false }); - expect(vmGroup.createLabelTitle).toBe('Create group label'); - vmGroup.$destroy(); + expect(vm.createLabelTitle).toBe('Create group label'); }); }); @@ -59,11 +65,9 @@ describe('BaseComponent', () => { }); it('return `Manage group labels` when `isProject` prop is false', () => { - const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false }); - const vmGroup = createComponent(mockConfigGroup); + wrapper.setProps({ isProject: false }); - expect(vmGroup.manageLabelsTitle).toBe('Manage group labels'); - vmGroup.$destroy(); + expect(vm.manageLabelsTitle).toBe('Manage group labels'); }); }); }); @@ -71,7 +75,7 @@ describe('BaseComponent', () => { describe('methods', () => { describe('handleClick', () => { it('emits onLabelClick event with label and list of labels as params', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleClick(mockLabels[0]); expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); @@ -80,7 +84,7 @@ describe('BaseComponent', () => { describe('handleCollapsedValueClick', () => { it('emits toggleCollapse event on component', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleCollapsedValueClick(); expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse'); @@ -89,7 +93,7 @@ describe('BaseComponent', () => { describe('handleDropdownHidden', () => { it('emits onDropdownClose event on component', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleDropdownHidden(); expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose'); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js index 0689fc1cf1f..bb33dc6ea0f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -2,9 +2,11 @@ import Vue from 'vue'; import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig, mockLabels } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { + mockConfig, + mockLabels, +} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const componentConfig = Object.assign({}, mockConfig, { fieldName: 'label_id[]', diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js index b8f32f96332..1c25d42682c 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -2,9 +2,8 @@ import Vue from 'vue'; import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockSuggestedColors } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = headerTitle => { const Component = Vue.extend(dropdownCreateLabelComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js index 3711e9dac8c..989901a0012 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js @@ -2,9 +2,8 @@ import Vue from 'vue'; import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = ( labelsWebUrl = mockConfig.labelsWebUrl, diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js index 115e21e4f9f..c36a82e1a35 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; const createComponent = () => { const Component = Vue.extend(dropdownHeaderComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js index c30e619e76b..2fffb2e495e 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; const createComponent = () => { const Component = Vue.extend(dropdownSearchInputComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js index 6c84d2e167c..1616e657c81 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; const createComponent = (canEdit = true) => { const Component = Vue.extend(dropdownTitleComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 4d3de5e474d..517f2c01c46 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -2,9 +2,8 @@ import Vue from 'vue'; import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockLabels } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = (labels = mockLabels) => { const Component = Vue.extend(dropdownValueCollapsedComponent); @@ -72,7 +71,7 @@ describe('DropdownValueCollapsedComponent', () => { describe('methods', () => { describe('handleClick', () => { it('emits onValueClick event on component', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleClick(); expect(vm.$emit).toHaveBeenCalledWith('onValueClick'); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 35a9c300953..ec143fec5d9 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -3,9 +3,11 @@ import $ from 'jquery'; import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig, mockLabels } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { + mockConfig, + mockLabels, +} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = ( labels = mockLabels, diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js index c911a129173..5cf25ca6f81 100644 --- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('toggleSidebar', () => { let vm; @@ -23,7 +23,7 @@ describe('toggleSidebar', () => { }); it('should emit toggle event when button clicked', () => { - const toggle = jasmine.createSpy(); + const toggle = jest.fn(); vm.$on('toggle', toggle); vm.$el.click(); diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb new file mode 100644 index 00000000000..3dd5b602aa2 --- /dev/null +++ b/spec/graphql/types/group_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['Group'] do + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) } + + it { expect(described_class.graphql_name).to eq('Group') } + + it { expect(described_class).to require_graphql_authorizations(:read_group) } +end diff --git a/spec/graphql/types/namespace_type.rb b/spec/graphql/types/namespace_type.rb new file mode 100644 index 00000000000..7cd6a79ae5d --- /dev/null +++ b/spec/graphql/types/namespace_type.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['Namespace'] do + it { expect(described_class.graphql_name).to eq('Namespace') } +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 69e3ea8a4a9..b4626955816 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do expect(described_class.graphql_name).to eq('Query') end - it { is_expected.to have_graphql_fields(:project, :echo, :metadata) } + it { is_expected.to have_graphql_fields(:project, :group, :echo, :metadata) } describe 'project field' do subject { described_class.fields['project'] } diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 11ab6c38a55..966aee72abb 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -113,7 +113,6 @@ describe('Issue', function() { mock = new MockAdapter(axios); mock.onGet(/(.*)\/related_branches$/).reply(200, {}); - mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {}); findElements(isIssueInitiallyOpen); this.issue = new Issue(); diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js index a503a54029f..6b9fe923624 100644 --- a/spec/javascripts/raven/index_spec.js +++ b/spec/javascripts/raven/index_spec.js @@ -5,19 +5,19 @@ describe('RavenConfig options', () => { const sentryDsn = 'sentryDsn'; const currentUserId = 'currentUserId'; const gitlabUrl = 'gitlabUrl'; - const isProduction = 'isProduction'; + const environment = 'test'; const revision = 'revision'; let indexReturnValue; beforeEach(() => { window.gon = { sentry_dsn: sentryDsn, + sentry_environment: environment, current_user_id: currentUserId, gitlab_url: gitlabUrl, revision, }; - process.env.NODE_ENV = isProduction; process.env.HEAD_COMMIT_SHA = revision; spyOn(RavenConfig, 'init'); @@ -25,12 +25,12 @@ describe('RavenConfig options', () => { indexReturnValue = index(); }); - it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => { + it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => { expect(RavenConfig.init).toHaveBeenCalledWith({ sentryDsn, currentUserId, - whitelistUrls: [gitlabUrl], - isProduction, + whitelistUrls: [gitlabUrl, 'webpack-internal://'], + environment, release: revision, tags: { revision, diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js index 5cc59cc28d3..af634a0c196 100644 --- a/spec/javascripts/raven/raven_config_spec.js +++ b/spec/javascripts/raven/raven_config_spec.js @@ -69,8 +69,8 @@ describe('RavenConfig', () => { let ravenConfig; const options = { sentryDsn: '//sentryDsn', - whitelistUrls: ['//gitlabUrl'], - isProduction: true, + whitelistUrls: ['//gitlabUrl', 'webpack-internal://'], + environment: 'test', release: 'revision', tags: { revision: 'revision', @@ -95,7 +95,7 @@ describe('RavenConfig', () => { release: options.release, tags: options.tags, whitelistUrls: options.whitelistUrls, - environment: 'production', + environment: 'test', ignoreErrors: ravenConfig.IGNORE_ERRORS, ignoreUrls: ravenConfig.IGNORE_URLS, shouldSendCallback: jasmine.any(Function), @@ -106,8 +106,8 @@ describe('RavenConfig', () => { expect(raven.install).toHaveBeenCalled(); }); - it('should set .environment to development if isProduction is false', () => { - ravenConfig.options.isProduction = false; + it('should set environment from options', () => { + ravenConfig.options.environment = 'development'; RavenConfig.configure.call(ravenConfig); diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 66cd8171c12..32b90041c64 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -191,9 +191,8 @@ describe Gitlab::Danger::Helper do expect(helper.changes_by_category).to eq( backend: %w[foo.rb], database: %w[db/foo], - docs: %w[foo.md], frontend: %w[foo.js], - none: %w[ee/changelogs/foo.yml], + none: %w[ee/changelogs/foo.yml foo.md], qa: %w[qa/foo], unknown: %w[foo] ) @@ -202,13 +201,13 @@ describe Gitlab::Danger::Helper do describe '#category_for_file' do where(:path, :expected_category) do - 'doc/foo' | :docs - 'CONTRIBUTING.md' | :docs - 'LICENSE' | :docs - 'MAINTENANCE.md' | :docs - 'PHILOSOPHY.md' | :docs - 'PROCESS.md' | :docs - 'README.md' | :docs + 'doc/foo' | :none + 'CONTRIBUTING.md' | :none + 'LICENSE' | :none + 'MAINTENANCE.md' | :none + 'PHILOSOPHY.md' | :none + 'PROCESS.md' | :none + 'README.md' | :none 'ee/doc/foo' | :unknown 'ee/README' | :unknown @@ -272,8 +271,8 @@ describe Gitlab::Danger::Helper do 'foo/bar.rb' | :backend 'foo/bar.js' | :frontend - 'foo/bar.txt' | :docs - 'foo/bar.md' | :docs + 'foo/bar.txt' | :none + 'foo/bar.md' | :none end with_them do diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index 0c4decc6518..46ad674a1eb 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -23,9 +23,12 @@ describe Gitlab::DataBuilder::Push do describe '.build' do let(:data) do - described_class.build(project, user, Gitlab::Git::BLANK_SHA, - '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', - 'refs/tags/v1.1.0') + described_class.build( + project: project, + user: user, + oldrev: Gitlab::Git::BLANK_SHA, + newrev: '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', + ref: 'refs/tags/v1.1.0') end it { expect(data).to be_a(Hash) } @@ -47,7 +50,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } + expect { described_class.build(project: spy, user: spy, ref: 'refs/tags/v1.1.0', commits: nil) } .not_to raise_error end end diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb index 0d5069568e1..6511c2b61bf 100644 --- a/spec/lib/gitlab/git/object_pool_spec.rb +++ b/spec/lib/gitlab/git/object_pool_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' describe Gitlab::Git::ObjectPool do + include RepoHelpers + let(:pool_repository) { create(:pool_repository) } let(:source_repository) { pool_repository.source_project.repository } + let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) } + let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) } subject { pool_repository.object_pool } @@ -76,4 +80,41 @@ describe Gitlab::Git::ObjectPool do end end end + + describe '#fetch' do + let(:commit_count) { source_repository.commit_count } + + context "when the object's pool repository exists" do + it 'does not raise an error' do + expect { subject.fetch }.not_to raise_error + end + end + + context "when the object's pool repository does not exist" do + before do + subject.delete + end + + it "re-creates the object pool's repository" do + subject.fetch + + expect(subject.repository.exists?).to be(true) + end + + it 'does not raise an error' do + expect { subject.fetch }.not_to raise_error + end + + it 'fetches objects from the source repository' do + new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid + + expect(subject.repository.exists?).to be false + + subject.fetch + + expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count) + expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id) + end + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5f8a2848944..0f6aac9b6de 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe Gitlab::Git::Repository, :seed_helper do include Gitlab::EncodingHelper + include RepoHelpers using RSpec::Parameterized::TableSyntax shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method| @@ -2209,83 +2210,6 @@ describe Gitlab::Git::Repository, :seed_helper do repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) end - # Build the options hash that's passed to Rugged::Commit#create - def commit_options(repo, index, target, ref, message) - options = {} - options[:tree] = index.write_tree(repo) - options[:author] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:committer] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:message] ||= message - options[:parents] = repo.empty? ? [] : [target].compact - options[:update_ref] = ref - - options - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of CHANGELOG with a single new line of text. - def new_commit_edit_old_file(repo) - oid = repo.write("I replaced the changelog with this text", :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "CHANGELOG", oid: oid, mode: 0100644) - - options = commit_options( - repo, - index, - repo.head.target, - "HEAD", - "Edit CHANGELOG in its original location" - ) - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of the specified file_path with new text. - def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) - oid = repo.write(text, :blob) - index = repo.index - index.read_tree(branch.target.tree) - index.add(path: file_path, oid: oid, mode: 0100644) - options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of encoding/CHANGELOG with new text. - def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) - branch = repo.branches[branch_name] - new_commit_edit_new_file(repo, file_path, commit_message, text, branch) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Moves the - # CHANGELOG file to the encoding/ directory. - def new_commit_move_file(repo) - blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] - file_content = repo.lookup(blob_oid).content - oid = repo.write(file_content, :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) - index.remove("CHANGELOG") - - options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - def refs(dir) IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| line.split("\t").last diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb index 149b7ec5bb0..0e0c3d329b5 100644 --- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb @@ -43,4 +43,24 @@ describe Gitlab::GitalyClient::ObjectPoolService do end end end + + describe '#fetch' do + before do + subject.delete + end + + it 'restores the pool repository objects' do + subject.fetch(project.repository) + + expect(object_pool.repository.exists?).to be(true) + end + + context 'when called twice' do + it "doesn't raise an error" do + subject.delete + + expect { subject.fetch(project.repository) }.not_to raise_error + end + end + end end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 8433d40b2ea..24ce397ec3d 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -33,6 +33,28 @@ describe Gitlab::Kubernetes::Helm::Api do end end + describe '#uninstall' do + before do + allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) + allow(namespace).to receive(:ensure_exists!).once + end + + it 'ensures the namespace exists before creating the POD' do + expect(namespace).to receive(:ensure_exists!).once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.uninstall(command) + end + + it 'removes an existing pod before installing' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.uninstall(command) + end + end + describe '#install' do before do allow(client).to receive(:create_pod).and_return(nil) diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb index 78c74266c61..048f4af6020 100644 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Gitlab::Prometheus::QueryVariables do describe '.call' do + let(:project) { environment.project } let(:environment) { create(:environment) } let(:slug) { environment.slug } @@ -21,13 +22,32 @@ describe Gitlab::Prometheus::QueryVariables do end context 'with deployment platform' do - let(:kube_namespace) { environment.deployment_platform.actual_namespace } + context 'with project cluster' do + let(:kube_namespace) { environment.deployment_platform.actual_namespace } - before do - create(:cluster, :provided_by_user, projects: [environment.project]) + before do + create(:cluster, :project, :provided_by_user, projects: [project]) + end + + it { is_expected.to include(kube_namespace: kube_namespace) } end - it { is_expected.to include(kube_namespace: kube_namespace) } + context 'with group cluster' do + let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) } + let(:group) { create(:group) } + let(:project2) { create(:project) } + let(:kube_namespace) { k8s_ns.namespace } + + let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) } + let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) } + + before do + group.projects << project + group.projects << project2 + end + + it { is_expected.to include(kube_namespace: kube_namespace) } + end end end end diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 44b5af5e5aa..eb32198265b 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -10,6 +10,8 @@ describe Ci::Bridge do create(:ci_bridge, pipeline: pipeline) end + it { is_expected.to include_module(Ci::PipelineDelegator) } + describe '#tags' do it 'only has a bridge tag' do expect(bridge.tags).to eq [:bridge] diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 339483d4f7d..59ec7310391 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -28,6 +28,7 @@ describe Ci::Build do it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } + it { is_expected.to include_module(Ci::PipelineDelegator) } it { is_expected.to be_a(ArtifactMigratable) } @@ -2273,6 +2274,19 @@ describe Ci::Build do it { user_variables.each { |v| is_expected.to include(v) } } end + context 'when build belongs to a pipeline for merge request' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_branch: 'improve/awesome') } + let(:pipeline) { merge_request.all_pipelines.first } + let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) } + + it 'returns values based on source ref' do + is_expected.to include( + { key: 'CI_COMMIT_REF_NAME', value: 'improve/awesome', public: true, masked: false }, + { key: 'CI_COMMIT_REF_SLUG', value: 'improve-awesome', public: true, masked: false } + ) + end + end + context 'when build has an environment' do let(:environment_variables) do [ @@ -2664,6 +2678,8 @@ describe Ci::Build do ) end + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + it 'returns static predefined variables' do expect(build.variables.size).to be >= 28 expect(build.variables) @@ -2713,6 +2729,8 @@ describe Ci::Build do ) end + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + it 'does not persist the build' do expect(build).to be_valid expect(build).not_to be_persisted diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3c823b78be7..9d0cd654f13 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -382,6 +382,54 @@ describe Ci::Pipeline, :mailer do end end + describe '#source_ref' do + subject { pipeline.source_ref } + + let(:pipeline) { create(:ci_pipeline, ref: 'feature') } + + it 'returns source ref' do + is_expected.to eq('feature') + end + + context 'when the pipeline is a detached merge request pipeline' do + let(:merge_request) { create(:merge_request) } + + let(:pipeline) do + create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path) + end + + it 'returns source ref' do + is_expected.to eq(merge_request.source_branch) + end + end + end + + describe '#source_ref_slug' do + subject { pipeline.source_ref_slug } + + let(:pipeline) { create(:ci_pipeline, ref: 'feature') } + + it 'slugifies with the source ref' do + expect(Gitlab::Utils).to receive(:slugify).with('feature') + + subject + end + + context 'when the pipeline is a detached merge request pipeline' do + let(:merge_request) { create(:merge_request) } + + let(:pipeline) do + create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path) + end + + it 'slugifies with the source ref of the merge request' do + expect(Gitlab::Utils).to receive(:slugify).with(merge_request.source_branch) + + subject + end + end + end + describe '.triggered_for_branch' do subject { described_class.triggered_for_branch(ref) } diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb index 5cd80edb3a1..8d853a04e33 100644 --- a/spec/models/clusters/applications/cert_manager_spec.rb +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -10,6 +10,12 @@ describe Clusters::Applications::CertManager do include_examples 'cluster application version specs', :clusters_applications_cert_managers include_examples 'cluster application initial status specs' + describe '#can_uninstall?' do + subject { cert_manager.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#install_command' do let(:cert_email) { 'admin@example.com' } diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index f177d493a2e..6ea6c110d62 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -18,6 +18,14 @@ describe Clusters::Applications::Helm do it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } end + describe '#can_uninstall?' do + let(:helm) { create(:clusters_applications_helm) } + + subject { helm.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#issue_client_cert' do let(:application) { create(:clusters_applications_helm) } subject { application.issue_client_cert } diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 113d29b5551..292ddabd2d8 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -18,6 +18,12 @@ describe Clusters::Applications::Ingress do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end + describe '#can_uninstall?' do + subject { ingress.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#make_installed!' do before do application.make_installed! diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 1a7363b64f9..fc9ebed863e 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -10,6 +10,15 @@ describe Clusters::Applications::Jupyter do it { is_expected.to belong_to(:oauth_application) } + describe '#can_uninstall?' do + let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') } + let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) } + + subject { jupyter.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#set_initial_status' do before do jupyter.set_initial_status diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 405b5ad691c..d5974f47190 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -39,6 +39,12 @@ describe Clusters::Applications::Knative do end end + describe '#can_uninstall?' do + subject { knative.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#schedule_status_update with external_ip' do let(:application) { create(:clusters_applications_knative, :installed) } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index e8ba9737c23..26267c64112 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -11,6 +11,21 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application initial status specs' + describe 'after_destroy' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } + let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let!(:prometheus_service) { project.create_prometheus_service(active: true) } + + it 'deactivates prometheus_service after destroy' do + expect do + application.destroy! + + prometheus_service.reload + end.to change(prometheus_service, :active).from(true).to(false) + end + end + describe 'transition to installed' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } @@ -23,12 +38,20 @@ describe Clusters::Applications::Prometheus do end it 'ensures Prometheus service is activated' do - expect(prometheus_service).to receive(:update).with(active: true) + expect(prometheus_service).to receive(:update!).with(active: true) subject.make_installed end end + describe '#can_uninstall?' do + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.can_uninstall? } + + it { is_expected.to be_truthy } + end + describe '#prometheus_client' do context 'cluster is nil' do it 'returns nil' do @@ -134,6 +157,34 @@ describe Clusters::Applications::Prometheus do end end + describe '#uninstall_command' do + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.uninstall_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + + it 'has the application name' do + expect(subject.name).to eq('prometheus') + end + + it 'has files' do + expect(subject.files).to eq(prometheus.files) + end + + it 'is rbac' do + expect(subject).to be_rbac + end + + context 'on a non rbac enabled cluster' do + before do + prometheus.cluster.platform_kubernetes.abac! + end + + it { is_expected.not_to be_rbac } + end + end + describe '#upgrade_command' do let(:prometheus) { build(:clusters_applications_prometheus) } let(:values) { prometheus.values } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index b66acf13135..bdc0cb8ed86 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -13,6 +13,14 @@ describe Clusters::Applications::Runner do it { is_expected.to belong_to(:runner) } + describe '#can_uninstall?' do + let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } + + subject { gitlab_runner.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#install_command' do let(:kubeclient) { double('kubernetes client') } let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f61857ea5ff..fb7f43b25cf 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2262,6 +2262,50 @@ describe MergeRequest do end end + describe "#environments" do + subject { merge_request.environments } + + let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + let(:project) { merge_request.project } + + let(:pipeline) do + create(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, project: project, + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) } + + it 'returns environments' do + is_expected.to eq(pipeline.environments) + expect(subject.count).to be(1) + end + + context 'when pipeline is not associated with environments' do + let!(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + it 'returns empty array' do + is_expected.to be_empty + end + end + + context 'when pipeline is not a pipeline for merge request' do + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: 'feature', + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + it 'returns empty relation' do + is_expected.to be_empty + end + end + end + describe "#reload_diff" do it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do user = create(:user) diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 142ddebbbf8..ec4d4517f82 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -344,4 +344,32 @@ describe PagesDomain do end end end + + describe '.for_removal' do + subject { described_class.for_removal } + + context 'when domain is not schedule for removal' do + let!(:domain) { create :pages_domain } + + it 'does not return domain' do + is_expected.to be_empty + end + end + + context 'when domain is scheduled for removal yesterday' do + let!(:domain) { create :pages_domain, remove_at: 1.day.ago } + + it 'returns domain' do + is_expected.to eq([domain]) + end + end + + context 'when domain is scheduled for removal tomorrow' do + let!(:domain) { create :pages_domain, remove_at: 1.day.from_now } + + it 'does not return domain' do + is_expected.to be_empty + end + end + end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index fd9e33c1781..a04b984c1f6 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -98,12 +98,11 @@ describe HipchatService do context 'tag_push events' do let(:push_sample_data) do Gitlab::DataBuilder::Push.build( - project, - user, - Gitlab::Git::BLANK_SHA, - '1' * 40, - 'refs/tags/test', - []) + project: project, + user: user, + oldrev: Gitlab::Git::BLANK_SHA, + newrev: '1' * 40, + ref: 'refs/tags/test') end it "calls Hipchat API for tag push events" do diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index e5f08aeb1fa..451dc88880c 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -439,6 +439,52 @@ describe MergeRequestPresenter do end end + describe '#source_branch_link' do + subject { presenter.source_branch_link } + + let(:presenter) { described_class.new(resource, current_user: user) } + + context 'when source branch exists' do + it 'returns link' do + allow(resource).to receive(:source_branch_exists?) { true } + + is_expected + .to eq("<a class=\"ref-name\" href=\"#{presenter.source_branch_commits_path}\">#{presenter.source_branch}</a>") + end + end + + context 'when source branch does not exist' do + it 'returns text' do + allow(resource).to receive(:source_branch_exists?) { false } + + is_expected.to eq("<span class=\"ref-name\">#{presenter.source_branch}</span>") + end + end + end + + describe '#target_branch_link' do + subject { presenter.target_branch_link } + + let(:presenter) { described_class.new(resource, current_user: user) } + + context 'when target branch exists' do + it 'returns link' do + allow(resource).to receive(:target_branch_exists?) { true } + + is_expected + .to eq("<a class=\"ref-name\" href=\"#{presenter.target_branch_commits_path}\">#{presenter.target_branch}</a>") + end + end + + context 'when target branch does not exist' do + it 'returns text' do + allow(resource).to receive(:target_branch_exists?) { false } + + is_expected.to eq("<span class=\"ref-name\">#{presenter.target_branch}</span>") + end + end + end + describe '#source_branch_with_namespace_link' do subject do described_class.new(resource, current_user: user).source_branch_with_namespace_link diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb new file mode 100644 index 00000000000..8ff95cc9af2 --- /dev/null +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Based on spec/requests/api/groups_spec.rb +# Should follow closely in order to ensure all situations are covered +describe 'getting group information' do + include GraphqlHelpers + include UploadHelpers + + let(:user1) { create(:user, can_create_group: false) } + let(:user2) { create(:user) } + let(:admin) { create(:admin) } + let(:public_group) { create(:group, :public) } + let(:private_group) { create(:group, :private) } + + # similar to the API "GET /groups/:id" + describe "Query group(fullPath)" do + def group_query(group) + graphql_query_for('group', 'fullPath' => group.full_path) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(group_query(public_group)) + end + end + + context 'when unauthenticated' do + it 'returns nil for a private group' do + post_graphql(group_query(private_group)) + + expect(graphql_data['group']).to be_nil + end + + it 'returns a public group' do + post_graphql(group_query(public_group)) + + expect(graphql_data['group']).not_to be_nil + end + end + + context "when authenticated as user" do + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let!(:group2) { create(:group, :private) } + + before do + group1.add_owner(user1) + group2.add_owner(user2) + end + + it "returns one of user1's groups" do + project = create(:project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + + post_graphql(group_query(group1), current_user: user1) + + expect(response).to have_gitlab_http_status(200) + expect(graphql_data['group']['id']).to eq(group1.id.to_s) + expect(graphql_data['group']['name']).to eq(group1.name) + expect(graphql_data['group']['path']).to eq(group1.path) + expect(graphql_data['group']['description']).to eq(group1.description) + expect(graphql_data['group']['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level)) + expect(graphql_data['group']['avatarUrl']).to eq(group1.avatar_url(only_path: false)) + expect(graphql_data['group']['webUrl']).to eq(group1.web_url) + expect(graphql_data['group']['requestAccessEnabled']).to eq(group1.request_access_enabled) + expect(graphql_data['group']['fullName']).to eq(group1.full_name) + expect(graphql_data['group']['fullPath']).to eq(group1.full_path) + expect(graphql_data['group']['parentId']).to eq(group1.parent_id) + end + + it "does not return a non existing group" do + query = graphql_query_for('group', 'fullPath' => '1328') + + post_graphql(query, current_user: user1) + + expect(graphql_data['group']).to be_nil + end + + it "does not return a group not attached to user1" do + private_group.add_owner(user2) + + post_graphql(group_query(private_group), current_user: user1) + + expect(graphql_data['group']).to be_nil + end + + it 'avoids N+1 queries' do + post_graphql(group_query(group1), current_user: admin) + + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(group_query(group1), current_user: admin) + end.count + + create(:project, namespace: group1) + + expect do + post_graphql(group_query(group1), current_user: admin) + end.not_to exceed_query_limit(control_count) + end + end + + context "when authenticated as admin" do + it "returns any existing group" do + post_graphql(group_query(private_group), current_user: admin) + + expect(graphql_data['group']['name']).to eq(private_group.name) + end + + it "does not return a non existing group" do + query = graphql_query_for('group', 'fullPath' => '1328') + post_graphql(query, current_user: admin) + + expect(graphql_data['group']).to be_nil + end + end + end +end diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index 7e151c3744e..f38a18fcf59 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -21,6 +21,10 @@ describe ClusterApplicationEntity do expect(subject[:status_reason]).to be_nil end + it 'has can_uninstall' do + expect(subject[:can_uninstall]).to be_falsey + end + context 'non-helm application' do let(:application) { build(:clusters_applications_runner, version: '0.0.0') } diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index 31b8d540356..890fa5bc009 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -105,6 +105,82 @@ describe Ci::StopEnvironmentsService do end end + describe '#execute_for_merge_request' do + subject { service.execute_for_merge_request(merge_request) } + + let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + let(:project) { merge_request.project } + let(:user) { create(:user) } + + let(:pipeline) do + create(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, + project: project, + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) } + let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) } + + before do + review_job.deployment.success! + end + + it 'has active environment at first' do + expect(pipeline.environments.first).to be_available + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it 'stops the active environment' do + subject + + expect(pipeline.environments.first).to be_stopped + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it 'does not stop the active environment' do + subject + + expect(pipeline.environments.first).to be_available + end + end + + context 'when pipeline is not associated with environments' do + let!(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + it 'does not raise exception' do + expect { subject }.not_to raise_exception + end + end + + context 'when pipeline is not a pipeline for merge request' do + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: 'feature', + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + it 'does not stop the active environment' do + subject + + expect(pipeline.environments.first).to be_available + end + end + end + def expect_environment_stopped_on(branch) expect_any_instance_of(Environment) .to receive(:stop!) diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 8ad90aaf720..a54bd85a11a 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -18,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context "when phase is #{a_phase}" do - context 'when not timeouted' do + context 'when not timed_out' do it 'reschedule a new check' do expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once expect(service).not_to receive(:remove_installation_pod) @@ -113,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context 'when timed out' do - let(:application) { create(:clusters_applications_helm, :timeouted, :updating) } + let(:application) { create(:clusters_applications_helm, :timed_out, :updating) } before do expect(service).to receive(:installation_phase).once.and_return(phase) @@ -174,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context 'when timed out' do - let(:application) { create(:clusters_applications_helm, :timeouted) } + let(:application) { create(:clusters_applications_helm, :timed_out) } before do expect(service).to receive(:installation_phase).once.and_return(phase) diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb new file mode 100644 index 00000000000..9ab83d913f5 --- /dev/null +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::CheckUninstallProgressService do + RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze + + let(:application) { create(:clusters_applications_prometheus, :uninstalling) } + let(:service) { described_class.new(application) } + let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN } + let(:errors) { nil } + let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } + + before do + allow(service).to receive(:installation_errors).and_return(errors) + allow(service).to receive(:remove_installation_pod) + end + + shared_examples 'a not yet terminated installation' do |a_phase| + let(:phase) { a_phase } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + context "when phase is #{a_phase}" do + context 'when not timed_out' do + it 'reschedule a new check' do + expect(worker_class).to receive(:perform_in).once + expect(service).not_to receive(:remove_installation_pod) + + expect do + service.execute + + application.reload + end.not_to change(application, :status) + + expect(application.status_reason).to be_nil + end + end + end + end + + context 'when application is installing' do + RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } + + context 'when installation POD succeeded' do + let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute + end + + it 'destroys the application' do + expect(worker_class).not_to receive(:perform_in) + + service.execute + + expect(application).to be_destroyed + end + + context 'an error occurs while destroying' do + before do + expect(application).to receive(:destroy!).once.and_raise("destroy failed") + end + + it 'still removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute + end + + it 'makes the application uninstall_errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed') + end + end + end + + context 'when installation POD failed' do + let(:phase) { Gitlab::Kubernetes::Pod::FAILED } + let(:errors) { 'test installation failed' } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'make the application errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.') + end + end + + context 'when timed out' do + let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'make the application errored' do + expect(worker_class).not_to receive(:perform_in) + + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.') + end + end + + context 'when installation raises a Kubeclient::HttpError' do + let(:cluster) { create(:cluster, :provided_by_user, :project) } + let(:logger) { service.send(:logger) } + let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) } + + before do + application.update!(cluster: cluster) + + expect(service).to receive(:installation_phase).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'Unauthorized' } + let(:error_code) { 401 } + end + + it 'shows the response code from the error' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Kubernetes error: 401') + end + end + end +end diff --git a/spec/services/clusters/applications/destroy_service_spec.rb b/spec/services/clusters/applications/destroy_service_spec.rb new file mode 100644 index 00000000000..8d9dc6a0f11 --- /dev/null +++ b/spec/services/clusters/applications/destroy_service_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::DestroyService, '#execute' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:user) { create(:user) } + let(:params) { { application: 'prometheus' } } + let(:service) { described_class.new(cluster, user, params) } + let(:test_request) { double } + let(:worker_class) { Clusters::Applications::UninstallWorker } + + subject { service.execute(test_request) } + + before do + allow(worker_class).to receive(:perform_async) + end + + context 'application is not installed' do + it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do + expect(worker_class).not_to receive(:perform_async) + + expect { subject } + .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError } + .and not_change { Clusters::Applications::Prometheus.count } + .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count } + end + end + + context 'application is installed' do + context 'application is schedulable' do + let!(:application) do + create(:clusters_applications_prometheus, :installed, cluster: cluster) + end + + it 'makes application scheduled!' do + subject + + expect(application.reload).to be_scheduled + end + + it 'schedules UninstallWorker' do + expect(worker_class).to receive(:perform_async).with(application.name, application.id) + + subject + end + end + + context 'application is not schedulable' do + let!(:application) do + create(:clusters_applications_prometheus, :updating, cluster: cluster) + end + + it 'raises StateMachines::InvalidTransition' do + expect(worker_class).not_to receive(:perform_async) + + expect { subject } + .to raise_exception { StateMachines::InvalidTransition } + .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count } + end + end + end +end diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb new file mode 100644 index 00000000000..16497d752b2 --- /dev/null +++ b/spec/services/clusters/applications/uninstall_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::UninstallService, '#execute' do + let(:application) { create(:clusters_applications_prometheus, :scheduled) } + let(:service) { described_class.new(application) } + let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } + let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } + + before do + allow(service).to receive(:helm_api).and_return(helm_client) + end + + context 'when there are no errors' do + before do + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)) + allow(worker_class).to receive(:perform_in).and_return(nil) + end + + it 'make the application to be uninstalling' do + expect(application.cluster).not_to be_nil + service.execute + + expect(application).to be_uninstalling + end + + it 'schedule async installation status check' do + expect(worker_class).to receive(:perform_in).once + + service.execute + end + end + + context 'when k8s cluster communication fails' do + let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'system failure' } + let(:error_code) { 500 } + end + + it 'make the application errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to match('Kubernetes error: 500') + end + end + + context 'a non kubernetes error happens' do + let(:application) { create(:clusters_applications_prometheus, :scheduled) } + let(:error) { StandardError.new('something bad happened') } + + before do + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'StandardError' } + let(:error_message) { 'something bad happened' } + let(:error_code) { nil } + end + + it 'make the application errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Failed to uninstall.') + end + end +end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index aa7dfda4950..ffa612cf315 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -74,6 +74,14 @@ describe MergeRequests::CloseService do .to change { project.open_merge_requests_count }.from(1).to(0) end + it 'clean up environments for the merge request' do + expect_next_instance_of(Ci::StopEnvironmentsService) do |service| + expect(service).to receive(:execute_for_merge_request).with(merge_request) + end + + described_class.new(project, user).execute(merge_request) + end + context 'current user is not authorized to close merge request' do before do perform_enqueued_jobs do diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 7b87913ab8b..ffc86f68469 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -62,5 +62,13 @@ describe MergeRequests::PostMergeService do expect(merge_request.reload).to be_merged end + + it 'clean up environments for the merge request' do + expect_next_instance_of(Ci::StopEnvironmentsService) do |service| + expect(service).to receive(:execute_for_merge_request).with(merge_request) + end + + described_class.new(project, user).execute(merge_request) + end end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 7f233a52f50..d9f9ede8ecd 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -5,15 +5,11 @@ require 'spec_helper' describe Projects::ImportService do let!(:project) { create(:project) } let(:user) { project.creator } - let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } - let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } subject { described_class.new(project, user) } before do allow(project).to receive(:lfs_enabled?).and_return(true) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute) - allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) end describe '#async?' do @@ -77,7 +73,6 @@ describe Projects::ImportService do context 'when repository creation succeeds' do it 'does not download lfs files' do expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -114,7 +109,6 @@ describe Projects::ImportService do context 'when repository import scheduled' do it 'does not download lfs objects' do expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -130,7 +124,7 @@ describe Projects::ImportService do it 'succeeds if repository import is successful' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) - expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({}) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :success) result = subject.execute @@ -146,6 +140,19 @@ describe Projects::ImportService do expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]" end + context 'when lfs import fails' do + it 'logs the error' do + error_message = 'error message' + + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) + expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message) + expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}") + + subject.execute + end + end + context 'when repository import scheduled' do before do allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) @@ -155,10 +162,7 @@ describe Projects::ImportService do it 'downloads lfs objects if lfs_enabled is enabled for project' do allow(project).to receive(:lfs_enabled?).and_return(true) - service = double - expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) - expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice - expect(service).to receive(:execute).twice + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute) subject.execute end @@ -166,7 +170,6 @@ describe Projects::ImportService do it 'does not download lfs objects if lfs_enabled is not enabled for project' do allow(project).to receive(:lfs_enabled?).and_return(false) expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -208,7 +211,6 @@ describe Projects::ImportService do allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true) expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -216,13 +218,22 @@ describe Projects::ImportService do it 'does not have a custom repository importer downloads lfs objects' do allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false) - service = double - expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) - expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice - expect(service).to receive(:execute).twice + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute) subject.execute end + + context 'when lfs import fails' do + it 'logs the error' do + error_message = 'error message' + + allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message) + expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}") + + subject.execute + end + end end end diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb index f1c0f5b9576..d8427d0bf78 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsDownloadLinkListService do @@ -85,7 +84,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do end describe '#get_download_links' do - it 'raise errorif request fails' do + it 'raise error if request fails' do allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request')) expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError) diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb index cde3f2d6155..f4470b50753 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsDownloadService do diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb index 5c9ca99df7c..7ca20a6d751 100644 --- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb @@ -1,148 +1,63 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsImportService do + let(:project) { create(:project) } + let(:user) { project.creator } let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } - let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"} - let(:group) { create(:group, lfs_enabled: true)} - let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) } - let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } - let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h } - let(:oids) { { 'oid1' => 123, 'oid2' => 125 } } let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } - let(:all_oids) { existing_lfs_objects.merge(oids) } - let(:remote_uri) { URI.parse(lfs_endpoint) } - - subject { described_class.new(project) } - - before do - allow(project.repository).to receive(:lfsconfig_for).and_return(nil) - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) - allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids) - end - - describe '#execute' do - context 'when no lfs pointer is linked' do - before do - allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([]) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) - expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original - end - - it 'retrieves all lfs pointers in the project repository' do - expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute) - - subject.execute - end - - it 'links existent lfs objects to the project' do - expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute) - - subject.execute - end - it 'retrieves the download links of non existent objects' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids) + subject { described_class.new(project, user) } - subject.execute - end + context 'when lfs is enabled for the project' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) end - context 'when some lfs objects are linked' do - before do - allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) - end + it 'downloads lfs objects' do + service = double + expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return(oid_download_links) + expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice + expect(service).to receive(:execute).twice - it 'retrieves the download links of non existent objects' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids) + result = subject.execute - subject.execute - end + expect(result[:status]).to eq :success end - context 'when all lfs objects are linked' do - before do - allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute) - end + context 'when no downloadable lfs object links' do + it 'does not call LfsDownloadService' do + expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return({}) + expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new) - it 'retrieves no download links' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original + result = subject.execute - expect(subject.execute).to be_empty + expect(result[:status]).to eq :success end end - context 'when lfsconfig file exists' do - before do - allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n") - end - - context 'when url points to the same import url host' do - let(:lfs_endpoint) { "#{import_url}/different_endpoint" } - let(:service) { double } - - before do - allow(service).to receive(:execute) - end - it 'downloads lfs object using the new endpoint' do - expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service) - - subject.execute - end - - context 'when import url has credentials' do - let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'} - - it 'adds the credentials to the new endpoint' do - expect(Projects::LfsPointers::LfsDownloadLinkListService) - .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint")) - .and_return(service) - - subject.execute - end - - context 'when url has its own credentials' do - let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" } + context 'when an exception is raised' do + it 'returns error' do + error_message = "error message" + expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_raise(StandardError, error_message) - it 'does not add the import url credentials' do - expect(Projects::LfsPointers::LfsDownloadLinkListService) - .to receive(:new).with(project, remote_uri: remote_uri) - .and_return(service) + result = subject.execute - subject.execute - end - end - end - end - - context 'when url points to a third party service' do - let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' } - - it 'disables lfs from the project' do - expect(project.lfs_enabled?).to be_truthy - - subject.execute - - expect(project.lfs_enabled?).to be_falsey - end - - it 'does not download anything' do - expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute) - - subject.execute - end + expect(result[:status]).to eq :error + expect(result[:message]).to eq error_message end end end - describe '#default_endpoint_uri' do - let(:import_url) { 'http://www.gitlab.com/demo/repo' } + context 'when lfs is not enabled for the project' do + it 'does not download lfs objects' do + allow(project).to receive(:lfs_enabled?).and_return(false) + expect(Projects::LfsPointers::LfsObjectDownloadListService).not_to receive(:new) + expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new) + + result = subject.execute - it 'adds suffix .git if the url does not have it' do - expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/) + expect(result[:status]).to eq :success end end end diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb index 5caa9de732e..849601c4a63 100644 --- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsLinkService do diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb new file mode 100644 index 00000000000..9dac29765a2 --- /dev/null +++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Projects::LfsPointers::LfsObjectDownloadListService do + let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } + let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"} + let(:group) { create(:group, lfs_enabled: true)} + let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) } + let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } + let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h } + let(:oids) { { 'oid1' => 123, 'oid2' => 125 } } + let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } + let(:all_oids) { existing_lfs_objects.merge(oids) } + let(:remote_uri) { URI.parse(lfs_endpoint) } + + subject { described_class.new(project) } + + before do + allow(project.repository).to receive(:lfsconfig_for).and_return(nil) + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids) + end + + describe '#execute' do + context 'when no lfs pointer is linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([]) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) + expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original + end + + it 'retrieves all lfs pointers in the project repository' do + expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute) + + subject.execute + end + + it 'links existent lfs objects to the project' do + expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute) + + subject.execute + end + + it 'retrieves the download links of non existent objects' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids) + + subject.execute + end + end + + context 'when some lfs objects are linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) + end + + it 'retrieves the download links of non existent objects' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids) + + subject.execute + end + end + + context 'when all lfs objects are linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute) + end + + it 'retrieves no download links' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original + + expect(subject.execute).to be_empty + end + end + + context 'when lfsconfig file exists' do + before do + allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n") + end + + context 'when url points to the same import url host' do + let(:lfs_endpoint) { "#{import_url}/different_endpoint" } + let(:service) { double } + + before do + allow(service).to receive(:execute) + end + + it 'downloads lfs object using the new endpoint' do + expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service) + + subject.execute + end + + context 'when import url has credentials' do + let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'} + + it 'adds the credentials to the new endpoint' do + expect(Projects::LfsPointers::LfsDownloadLinkListService) + .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint")) + .and_return(service) + + subject.execute + end + + context 'when url has its own credentials' do + let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" } + + it 'does not add the import url credentials' do + expect(Projects::LfsPointers::LfsDownloadLinkListService) + .to receive(:new).with(project, remote_uri: remote_uri) + .and_return(service) + + subject.execute + end + end + end + end + + context 'when url points to a third party service' do + let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' } + + it 'disables lfs from the project' do + expect(project.lfs_enabled?).to be_truthy + + subject.execute + + expect(project.lfs_enabled?).to be_falsey + end + + it 'does not download anything' do + expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute) + + subject.execute + end + end + end + end + + describe '#default_endpoint_uri' do + let(:import_url) { 'http://www.gitlab.com/demo/repo' } + + it 'adds suffix .git if the url does not have it' do + expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/) + end + end +end diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb index 1447b9d4126..2a553e18807 100644 --- a/spec/services/todos/destroy/entity_leave_service_spec.rb +++ b/spec/services/todos/destroy/entity_leave_service_spec.rb @@ -75,6 +75,13 @@ describe Todos::Destroy::EntityLeaveService do project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end + it 'enqueues the PrivateFeaturesWorker' do + expect(TodosDestroyer::PrivateFeaturesWorker) + .to receive(:perform_async).with(project.id, user.id) + + subject + end + context 'confidential issues' do context 'when a user is not an author of confidential issue' do it 'removes only confidential issues todos' do @@ -246,6 +253,13 @@ describe Todos::Destroy::EntityLeaveService do project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end + it 'enqueues the PrivateFeaturesWorker' do + expect(TodosDestroyer::PrivateFeaturesWorker) + .to receive(:perform_async).with(project.id, user.id) + + subject + end + context 'when user is not member' do it 'removes only confidential issues todos' do expect { subject }.to change { Todo.count }.from(5).to(4) diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index e5c7b5bb9a7..f2b3b44d223 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -57,12 +57,12 @@ describe VerifyPagesDomainService do expect(domain).not_to be_verified end - it 'disables domain and shedules it for removal' do - Timecop.freeze do - service.execute - expect(domain).not_to be_enabled - expect(domain.remove_at).to be_within(1.second).of(1.week.from_now) - end + it 'disables domain and shedules it for removal in 1 week' do + service.execute + + expect(domain).not_to be_enabled + + expect(domain.remove_at).to be_like_time(7.days.from_now) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8ca4c172707..fbc5fcea7b9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,6 +53,7 @@ RSpec.configure do |config| config.display_try_failure_messages = true config.infer_spec_type_from_file_location! + config.full_backtrace = true config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| location = metadata[:location] diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index 4af90f4af79..44d95a029af 100644 --- a/spec/support/helpers/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb @@ -11,6 +11,8 @@ module RepoHelpers # blob.path # => 'files/js/commit.js.coffee' # blob.data # => 'class Commit...' # + # Build the options hash that's passed to Rugged::Commit#create + def sample_blob OpenStruct.new( oid: '5f53439ca4b009096571d3c8bc3d09d30e7431b3', @@ -129,4 +131,80 @@ eos file_content: content ).execute end + + def commit_options(repo, index, target, ref, message) + options = {} + options[:tree] = index.write_tree(repo) + options[:author] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:committer] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:message] ||= message + options[:parents] = repo.empty? ? [] : [target].compact + options[:update_ref] = ref + + options + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of CHANGELOG with a single new line of text. + def new_commit_edit_old_file(repo) + oid = repo.write("I replaced the changelog with this text", :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "CHANGELOG", oid: oid, mode: 0100644) + + options = commit_options( + repo, + index, + repo.head.target, + "HEAD", + "Edit CHANGELOG in its original location" + ) + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of the specified file_path with new text. + def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) + oid = repo.write(text, :blob) + index = repo.index + index.read_tree(branch.target.tree) + index.add(path: file_path, oid: oid, mode: 0100644) + options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of encoding/CHANGELOG with new text. + def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) + branch = repo.branches[branch_name] + new_commit_edit_new_file(repo, file_path, commit_message, text, branch) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Moves the + # CHANGELOG file to the encoding/ directory. + def new_commit_move_file(repo) + blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] + file_content = repo.lookup(blob_oid).content + oid = repo.write(file_content, :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) + index.remove("CHANGELOG") + + options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end end diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb index e7ec24c5b7e..d8f7ba1185e 100644 --- a/spec/support/shared_examples/application_setting_examples.rb +++ b/spec/support/shared_examples/application_setting_examples.rb @@ -249,4 +249,41 @@ RSpec.shared_examples 'application settings examples' do expect(setting.password_authentication_enabled_for_web?).to be_falsey end + + describe 'sentry settings' do + context 'when the sentry settings are not set in gitlab.yml' do + it 'fallbacks to the settings in the database' do + setting.sentry_enabled = true + setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40' + setting.clientside_sentry_enabled = true + setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41' + + allow(Gitlab.config.sentry).to receive(:enabled).and_return(false) + allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil) + + expect(setting.sentry_enabled).to eq true + expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40' + expect(setting.clientside_sentry_enabled).to eq true + expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41' + end + end + + context 'when the sentry settings are set in gitlab.yml' do + it 'does not fallback to the settings in the database' do + setting.sentry_enabled = false + setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40' + setting.clientside_sentry_enabled = false + setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41' + + allow(Gitlab.config.sentry).to receive(:enabled).and_return(true) + allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42') + + expect(setting).not_to receive(:read_attribute) + expect(setting.sentry_enabled).to eq true + expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + expect(setting.clientside_sentry_enabled).to eq true + expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + end + end + end end diff --git a/spec/support/shared_examples/models/chat_service_spec.rb b/spec/support/shared_examples/models/chat_service_spec.rb index 9d3ce5e2be1..0a302e7d030 100644 --- a/spec/support/shared_examples/models/chat_service_spec.rb +++ b/spec/support/shared_examples/models/chat_service_spec.rb @@ -70,7 +70,7 @@ shared_examples_for "chat service" do |service_name| context "with not default branch" do let(:sample_data) do - Gitlab::DataBuilder::Push.build(project, user, nil, nil, "not-the-default-branch") + Gitlab::DataBuilder::Push.build(project: project, user: user, ref: "not-the-default-branch") end context "when notify_only_default_branch enabled" do diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb index 1f76b981292..d6490a808ce 100644 --- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb @@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name| it { is_expected.to belong_to(:cluster) } it { is_expected.to validate_presence_of(:cluster) } + describe '#can_uninstall?' do + it 'calls allowed_to_uninstall?' do + expect(subject).to receive(:allowed_to_uninstall?).and_return(true) + + expect(subject.can_uninstall?).to be_truthy + end + end + describe '#name' do it 'is .application_name' do expect(subject.name).to eq(described_class.application_name) diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index b8c19cab0c4..4525c03837f 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.status_reason).to eq(reason) end end + + context 'application is uninstalling' do + subject { create(application_name, :uninstalling) } + + it 'is uninstall_errored' do + subject.make_errored(reason) + + expect(subject).to be_uninstall_errored + expect(subject.status_reason).to eq(reason) + end + end end describe '#make_scheduled' do @@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_scheduled end + describe 'when installed' do + subject { create(application_name, :installed) } + + it 'is scheduled' do + subject.make_scheduled + + expect(subject).to be_scheduled + end + end + describe 'when was errored' do subject { create(application_name, :errored) } @@ -148,6 +169,28 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.status_reason).to be_nil end end + + describe 'when was uninstall_errored' do + subject { create(application_name, :uninstall_errored) } + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_scheduled! + + expect(subject.status_reason).to be_nil + end + end + end + + describe '#make_uninstalling' do + subject { create(application_name, :scheduled) } + + it 'is uninstalling' do + subject.make_uninstalling! + + expect(subject).to be_uninstalling + end end end @@ -155,16 +198,18 @@ shared_examples 'cluster application status specs' do |application_name| using RSpec::Parameterized::TableSyntax where(:trait, :available) do - :not_installable | false - :installable | false - :scheduled | false - :installing | false - :installed | true - :updating | false - :updated | true - :errored | false - :update_errored | false - :timeouted | false + :not_installable | false + :installable | false + :scheduled | false + :installing | false + :installed | true + :updating | false + :updated | true + :errored | false + :update_errored | false + :uninstalling | false + :uninstall_errored | false + :timed_out | false end with_them do diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb index c31346374f4..36c486dbdd6 100644 --- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb @@ -275,7 +275,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'does not notify push events if they are not for the default branch' do ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref) chat_service.execute(push_sample_data) @@ -292,7 +292,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'still notifies about pushed tags' do ref = "#{Gitlab::Git::TAG_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref) chat_service.execute(push_sample_data) @@ -307,7 +307,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'notifies about all push events' do ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref) chat_service.execute(push_sample_data) diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 9ce9a353913..a62830c35f1 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -771,6 +771,14 @@ describe ObjectStorage do expect { avatars }.not_to exceed_query_limit(1) end + it 'does not attempt to replace methods' do + models.each do |model| + expect(model.avatar.upload).to receive(:method_missing).and_call_original + + model.avatar.upload.path + end + end + it 'fetches a unique upload for each model' do expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url)) expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload)) diff --git a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb deleted file mode 100644 index 9424795749d..00000000000 --- a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -describe 'projects/issues/_merge_requests_status.html.haml' do - around do |ex| - Timecop.freeze(Date.new(2018, 7, 22)) do - ex.run - end - end - - it 'shows date of status change in tooltip' do - merge_request = create(:merge_request, created_at: 1.month.ago) - - render partial: 'projects/issues/merge_requests_status', - locals: { merge_request: merge_request, css_class: '' } - - expect(rendered).to match("Opened.*about 1 month ago") - end - - it 'shows only status in tooltip if date is not set' do - merge_request = create(:merge_request, state: :closed) - - render partial: 'projects/issues/merge_requests_status', - locals: { merge_request: merge_request, css_class: '' } - - expect(rendered).to match("Closed") - end -end diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb new file mode 100644 index 00000000000..aaf5c9defc4 --- /dev/null +++ b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do + let(:app) { create(:clusters_applications_helm) } + let(:app_name) { app.name } + let(:app_id) { app.id } + + subject { described_class.new.perform(app_name, app_id) } + + context 'app exists' do + let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) } + + it 'calls the check service' do + expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service) + expect(service).to receive(:execute).once + + subject + end + end + + context 'app does not exist' do + let(:app_id) { 0 } + + it 'does not call the check service' do + expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new) + + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb new file mode 100644 index 00000000000..0e1171e8491 --- /dev/null +++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PagesDomainRemovalCronWorker do + subject(:worker) { described_class.new } + + describe '#perform' do + context 'when there is domain which should be removed' do + let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) } + + before do + stub_feature_flags(remove_disabled_domains: true) + end + + it 'removes domain' do + expect { worker.perform }.to change { PagesDomain.count }.by(-1) + expect(PagesDomain.exists?).to eq(false) + end + + context 'when domain removal is disabled' do + before do + stub_feature_flags(remove_disabled_domains: false) + end + + it 'does not remove pages domain' do + expect { worker.perform }.not_to change { PagesDomain.count } + expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present + end + end + end + + context 'where there is a domain which scheduled for removal in the future' do + let!(:domain_for_removal) { create(:pages_domain, :scheduled_for_removal) } + + it 'does not remove pages domain' do + expect { worker.perform }.not_to change { PagesDomain.count } + expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present + end + end + end +end diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb index 9b479da1cb6..186824a444f 100644 --- a/spec/workers/pages_domain_verification_cron_worker_spec.rb +++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb @@ -6,11 +6,11 @@ describe PagesDomainVerificationCronWorker do subject(:worker) { described_class.new } describe '#perform' do - it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do - verified = create(:pages_domain) - reverify = create(:pages_domain, :reverify) - disabled = create(:pages_domain, :disabled) + let!(:verified) { create(:pages_domain) } + let!(:reverify) { create(:pages_domain, :reverify) } + let!(:disabled) { create(:pages_domain, :disabled) } + it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do [reverify, disabled].each do |domain| expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id) end diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index f23910d23be..8c604b13297 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe PipelineScheduleWorker do + include ExclusiveLeaseHelpers + subject { described_class.new.perform } set(:project) { create(:project, :repository) } @@ -39,6 +41,16 @@ describe PipelineScheduleWorker do it_behaves_like 'successful scheduling' + context 'when exclusive lease has already been taken by the other instance' do + before do + stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT) + end + + it 'raises an error and does not start creating pipelines' do + expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + end + end + context 'when the latest commit contains [ci skip]' do before do allow_any_instance_of(Ci::Pipeline) diff --git a/yarn.lock b/yarn.lock index a0446b652ac..7c9652ba131 100644 --- a/yarn.lock +++ b/yarn.lock @@ -663,12 +663,13 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087" integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g== -"@gitlab/ui@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.5.0.tgz#31ecfc16e3f7663545f31ddf07e02bba96a6d138" - integrity sha512-eDD++hhGJuH59g2QcGshuou9/NLcLfse4Abm9KOIWIaYI3NPWW2KRGwLHPB6H0d5W0/X5pyWYQvXgF7JE2ZXbA== +"@gitlab/ui@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.7.0.tgz#8d0892ae54ddcb3c309bd970c57a433af6098edf" + integrity sha512-DEIPfem9P5j0DyzZp0M62SbLQu1D4feiNO0oAYN8bJrgiMC8H3VEJwiyplNItSwFYa985O1xOr3B81eTiZEWDQ== dependencies: "@babel/standalone" "^7.0.0" + "@gitlab/vue-toasted" "^1.2.1" bootstrap-vue "^2.0.0-rc.11" copy-to-clipboard "^3.0.8" echarts "^4.2.0-rc.2" @@ -678,7 +679,11 @@ url-search-params-polyfill "^5.0.0" vue "^2.5.21" vue-loader "^15.4.2" - vue-toasted "^1.1.26" + +"@gitlab/vue-toasted@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@gitlab/vue-toasted/-/vue-toasted-1.2.1.tgz#f407b5aa710863e5b7f021f4a1f66160331ab263" + integrity sha512-ve2PLxKqrwNpsd+4bV5zGJT5+H5N/VJBZoFS2Vp1mH5cUDBYIHTzDmbS6AbBGUDh0F3TxmFMiqfXfpO/1VjBNQ== "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -2736,17 +2741,17 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -d3-array@1, d3-array@1.2.1, d3-array@^1.2.0, d3-array@^1.2.1: +d3-array@1, d3-array@1.2.1, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" integrity sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw== -d3-axis@1.0.8, d3-axis@^1.0.8: +d3-axis@1, d3-axis@1.0.8, d3-axis@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa" integrity sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo= -d3-brush@1.0.4, d3-brush@^1.0.4: +d3-brush@1, d3-brush@1.0.4, d3-brush@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4" integrity sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q= @@ -2757,7 +2762,7 @@ d3-brush@1.0.4, d3-brush@^1.0.4: d3-selection "1" d3-transition "1" -d3-chord@1.0.4: +d3-chord@1, d3-chord@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c" integrity sha1-fexPC6iG9xP+ERxF92NBT290yiw= @@ -2775,6 +2780,13 @@ d3-color@1, d3-color@1.0.3: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" integrity sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs= +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + d3-dispatch@1, d3-dispatch@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8" @@ -2802,7 +2814,14 @@ d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3: resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4= -d3-force@1.1.0: +d3-fetch@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.1.2.tgz#957c8fbc6d4480599ba191b1b2518bf86b3e1be2" + integrity sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA== + dependencies: + d3-dsv "1" + +d3-force@1, d3-force@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3" integrity sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg== @@ -2817,14 +2836,14 @@ d3-format@1, d3-format@1.2.2: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a" integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw== -d3-geo@1.9.1: +d3-geo@1, d3-geo@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356" integrity sha512-l9wL/cEQkyZQYXw3xbmLsH3eQ5ij+icNfo4r0GrLa5rOCZR/e/3am45IQ0FvQ5uMsv+77zBRunLc9ufTWSQYFA== dependencies: d3-array "1" -d3-hierarchy@1.1.5: +d3-hierarchy@1, d3-hierarchy@1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26" integrity sha1-ochFxC+Eoga88cAcAQmOpN2qeiY= @@ -2841,7 +2860,7 @@ d3-path@1, d3-path@1.0.5: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764" integrity sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q= -d3-polygon@1.0.3: +d3-polygon@1, d3-polygon@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62" integrity sha1-FoiOkCZGCTPysXllKtN4Ik04LGI= @@ -2856,7 +2875,7 @@ d3-queue@3.0.7: resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618" integrity sha1-yTouVLQXwJWRKdfXP2z31Ckudhg= -d3-random@1.1.0: +d3-random@1, d3-random@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3" integrity sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM= @@ -2871,6 +2890,14 @@ d3-request@1.0.6: d3-dsv "1" xmlhttprequest "1" +d3-scale-chromatic@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0" + integrity sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw== + dependencies: + d3-color "1" + d3-interpolate "1" + d3-scale@1.0.7, d3-scale@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" @@ -2884,12 +2911,24 @@ d3-scale@1.0.7, d3-scale@^1.0.7: d3-time "1" d3-time-format "2" +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA== -d3-shape@1.2.0, d3-shape@^1.2.0: +d3-shape@1, d3-shape@1.2.0, d3-shape@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777" integrity sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c= @@ -2925,12 +2964,12 @@ d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1: d3-selection "^1.1.0" d3-timer "1" -d3-voronoi@1.1.2: +d3-voronoi@1, d3-voronoi@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c" integrity sha1-Fodmfo8TotFYyAwUgMWinLDYlzw= -d3-zoom@1.7.1: +d3-zoom@1, d3-zoom@1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63" integrity sha512-sZHQ55DGq5BZBFGnRshUT8tm2sfhPHFnOlmPbbwTkAoPeVdRTkB4Xsf9GCY0TSHrTD8PeJPZGmP/TpGicwJDJQ== @@ -2977,6 +3016,43 @@ d3@^4.13.0: d3-voronoi "1.1.2" d3-zoom "1.7.1" +d3@^5.7.0: + version "5.9.2" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.2.tgz#64e8a7e9c3d96d9e6e4999d2c8a2c829767e67f5" + integrity sha512-ydrPot6Lm3nTWH+gJ/Cxf3FcwuvesYQ5uk+j/kXEH/xbuYWYWTMAHTJQkyeuG8Y5WM5RSEYB41EctUrXQQytRQ== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dagre-d3-renderer@^0.5.8: version "0.5.8" resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz#aa071bb71d3c4d67426925906f3f6ddead49c1a3" @@ -4938,10 +5014,10 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -he@^1.1.0, he@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +he@^1.1.0, he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== highlight.js@^9.13.1, highlight.js@~9.13.0: version "9.13.1" @@ -7008,19 +7084,19 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== -mermaid@^8.0.0-rc.8: - version "8.0.0-rc.8" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72" - integrity sha512-GbF9jHWfqE7YGx9vQySmBxy2Ahlclxmpk4tJ9ntNyafENl96s96ggUK/NQS5ydYoFab6MavTm4YMTIPKqWVvPQ== +mermaid@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0.tgz#8f6c75017e788a8c3997e20c5e5046c2b88d1a8f" + integrity sha512-vUQRykev0A6RtxIVqQT3a9TDxcSbdZbQF5JDyKgidnYuJy8BE8jp6LM+HKDSQuroKm6buu4NlpMO+qhxIP/cTg== dependencies: - d3 "^4.13.0" + d3 "^5.7.0" dagre-d3-renderer "^0.5.8" dagre-layout "^0.8.8" graphlibrary "^2.2.0" - he "^1.1.1" - lodash "^4.17.5" - moment "^2.21.0" - scope-css "^1.0.5" + he "^1.2.0" + lodash "^4.17.11" + moment "^2.23.0" + scope-css "^1.2.1" methods@~1.1.2: version "1.1.2" @@ -7172,10 +7248,10 @@ mkdirp@0.5.x, mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp dependencies: minimist "0.0.8" -moment@2.x, moment@^2.10.2, moment@^2.21.0: - version "2.23.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" - integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== +moment@2.x, moment@^2.10.2, moment@^2.23.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== monaco-editor-webpack-plugin@^1.7.0: version "1.7.0" @@ -9267,7 +9343,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -scope-css@^1.0.5: +scope-css@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/scope-css/-/scope-css-1.2.1.tgz#c35768bc900cad030a3e0d663a818c0f6a57f40e" integrity sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q== @@ -10983,11 +11059,6 @@ vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== -vue-toasted@^1.1.26: - version "1.1.26" - resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.26.tgz#1333d1a42157ab78389c3810023a49ba94e69c7b" - integrity sha512-Z4/gfPcqdzsRvif7UITrZOkh3C6jm0yQKJyr9kX31IGWXor5dNipE1Sc5SnlL5RLmY7vlLa+SqIjc9Gbpy7V0g== - vue-virtual-scroll-list@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76" |