diff options
author | John Jarvis <jarv@gitlab.com> | 2019-04-04 12:06:58 +0300 |
---|---|---|
committer | John Jarvis <jarv@gitlab.com> | 2019-04-04 12:06:58 +0300 |
commit | 83ab2898ed37abfd4bf7f92000a9899957811f33 (patch) | |
tree | 91a2a3ed4f59456d8a732a63ce6787f5bdba81b9 | |
parent | bfec819ff91f839f6352aac3347e2cbfd7ebf2f0 (diff) | |
parent | a6e9175fdd7790cc433ba49a85eaadbf75a3c8e9 (diff) |
Merge branch 'master' into 11-10-stable
177 files changed, 3725 insertions, 1973 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab38c87039e..f89a52e7a3e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1018,10 +1018,8 @@ schedule:review-build-cng: .review-deploy-base: &review-deploy-base <<: *review-base - retry: 2 allow_failure: true variables: - GIT_DEPTH: "1" HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" GITLAB_HELM_CHART_REF: "master" diff --git a/.gitlab/CODEOWNERS.disabled b/.gitlab/CODEOWNERS.disabled index b9f886c1d47..89a9696d3e8 100644 --- a/.gitlab/CODEOWNERS.disabled +++ b/.gitlab/CODEOWNERS.disabled @@ -1,6 +1,6 @@ # Backend Maintainers are the default for all ruby files -*.rb @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @nick.thomas @rspeicher @rymai @smcgivern -*.rake @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @nick.thomas @rspeicher @rymai @smcgivern +*.rb @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern +*.rake @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern # Technical writing team are the default reviewers for everything in `doc/` /doc/ @axil @marcia diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8123a5888..76e2ed66cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.9.5 (2019-04-03) + +### Fixed (3 changes) + +- Force to recreate all MR diffs on import. !26480 +- Fix API /project/:id/branches not returning correct merge status. !26785 +- Avoid excessive recursive calls with Rugged TreeEntries. !26813 + +### Performance (1 change) + +- Force a full GC after importing a project. !26803 + + ## 11.9.3 (2019-03-27) ### Security (8 changes) diff --git a/Dangerfile b/Dangerfile index 32f4b4d23c3..95dd48aae9e 100644 --- a/Dangerfile +++ b/Dangerfile @@ -13,3 +13,4 @@ danger.import_dangerfile(path: 'danger/prettier') danger.import_dangerfile(path: 'danger/eslint') danger.import_dangerfile(path: 'danger/roulette') danger.import_dangerfile(path: 'danger/single_codebase') +danger.import_dangerfile(path: 'danger/gitlab_ui_wg') diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index fb6e5291a61..45b9e57f9ab 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -54,7 +54,10 @@ export default Vue.extend({ return `${n__('%d issue', '%d issues', issuesSize)}`; }, isNewIssueShown() { - return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); + return ( + this.list.type === 'backlog' || + (!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank') + ); }, }, watch: { diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index 5b20fa141cd..da3100b9386 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -40,6 +40,12 @@ export default class VariableList { // converted. we need the value as a string. default: $('.js-ci-variable-input-protected').attr('data-default'), }, + masked: { + selector: '.js-ci-variable-input-masked', + // use `attr` instead of `data` as we don't want the value to be + // converted. we need the value as a string. + default: $('.js-ci-variable-input-masked').attr('data-default'), + }, environment_scope: { // We can't use a `.js-` class here because // gl_dropdown replaces the <input> and doesn't copy over the class @@ -88,13 +94,16 @@ export default class VariableList { } }); - // Always make sure there is an empty last row - this.$container.on('input trigger-change', inputSelector, () => { + this.$container.on('input trigger-change', inputSelector, e => { + // Always make sure there is an empty last row const $lastRow = this.$container.find('.js-row').last(); if (this.checkIfRowTouched($lastRow)) { this.insertRow($lastRow); } + + // If masked, validate value against regex + this.validateMaskability($(e.currentTarget).closest('.js-row')); }); } @@ -171,12 +180,33 @@ export default class VariableList { checkIfRowTouched($row) { return Object.keys(this.inputMap).some(name => { + // Row should not qualify as touched if only switches have been touched + if (['protected', 'masked'].includes(name)) return false; + const entry = this.inputMap[name]; const $el = $row.find(entry.selector); return $el.length && $el.val() !== entry.default; }); } + validateMaskability($row) { + const invalidInputClass = 'gl-field-error-outline'; + + const maskableRegex = /^\w{8,}$/; // Eight or more alphanumeric characters plus underscores + const variableValue = $row.find(this.inputMap.secret_value.selector).val(); + const isValueMaskable = maskableRegex.test(variableValue) || variableValue === ''; + const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true'; + + // Show a validation error if the user wants to mask an unmaskable variable value + $row + .find(this.inputMap.secret_value.selector) + .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable); + $row + .find('.js-secret-value-placeholder') + .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable); + $row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable); + } + toggleEnableRow(isEnabled = true) { this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled); this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 58a9605c181..f5876a73eff 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -188,10 +188,6 @@ export default { /> </div> </template> - <div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff"> - {{ __('This source diff could not be displayed because it is too large.') }} - <span v-html="viewBlobLink"></span> - </div> </div> </template> diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index 5dfba8fe531..df100f753d7 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -1,6 +1,8 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { decorateData, sortTree } from '../stores/utils'; +export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); + export const splitParent = path => { const idx = path.lastIndexOf('/'); @@ -45,7 +47,7 @@ export const decorateFiles = ({ id: path, name, path, - url: `/${projectId}/tree/${branchId}/-/${path}/`, + url: `/${projectId}/tree/${branchId}/-/${escapeFileUrl(path)}/`, type: 'tree', parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, @@ -81,7 +83,7 @@ export const decorateFiles = ({ id: path, name, path, - url: `/${projectId}/blob/${branchId}/-/${path}`, + url: `/${projectId}/blob/${branchId}/-/${escapeFileUrl(path)}`, type: 'blob', parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d3fe8f77bd4..4d6327840db 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -92,7 +92,7 @@ export const getTimeago = () => { const timeAgoLocaleRemaining = [ () => [s__('Timeago|just now'), s__('Timeago|right now')], - () => [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], + () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')], () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], @@ -121,7 +121,7 @@ export const getTimeago = () => { const timeAgoLocale = [ () => [s__('Timeago|just now'), s__('Timeago|right now')], - () => [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], + () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')], () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], @@ -160,7 +160,11 @@ export const getTimeago = () => { * @param {Boolean} setTimeago */ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { - getTimeago().render($timeagoEls, timeagoLanguageCode); + getTimeago(); + + $timeagoEls.each((i, el) => { + $(el).text(timeagoInstance.format($(el).attr('datetime'), timeagoLanguageCode)); + }); if (!setTimeago) { return; diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index fc73726857d..aabb77f6a85 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -86,9 +86,6 @@ export default { }, computed: { ...mapGetters(['getUserDataByProp']), - showReplyButton() { - return gon.features && gon.features.replyToIndividualNotes && this.showReply; - }, shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, @@ -167,7 +164,7 @@ export default { </a> </div> <reply-button - v-if="showReplyButton" + v-if="showReply" ref="replyButton" class="js-reply-button" @startReplying="$emit('startReplying')" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 5fa0ab3de98..d2cfeff53e8 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -96,7 +96,7 @@ export default { return ''; } - // We need to do this to ensure we have the currect sentence order + // We need to do this to ensure we have the correct sentence order // when translating this as the sentence order may change from one // language to the next. See: // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771 diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 6aed2492084..08545dcea46 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import Flash from './flash'; +import { __ } from '~/locale'; export default function notificationsDropdown() { $(document).on('click', '.update-notification', function updateNotificationCallback(e) { @@ -27,7 +28,7 @@ export default function notificationsDropdown() { .closest('.js-notification-dropdown') .replaceWith(data.html); } else { - Flash('Failed to save new settings', 'alert'); + Flash(__('Failed to save new settings'), 'alert'); } }); } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js index 95b57d5e048..c1f6edf2f27 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js @@ -1,17 +1,33 @@ -/* eslint-disable class-methods-use-this */ +const defaultTimezone = 'UTC'; + +export const formatUtcOffset = offset => { + const parsed = parseInt(offset, 10); + if (Number.isNaN(parsed) || parsed === 0) { + return `0`; + } + const prefix = offset > 0 ? '+' : '-'; + return `${prefix} ${Math.abs(offset / 3600)}`; +}; -import $ from 'jquery'; +export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; -const defaultTimezone = 'UTC'; +const defaults = { + $inputEl: null, + $dropdownEl: null, + onSelectTimezone: null, +}; export default class TimezoneDropdown { - constructor() { - this.$dropdown = $('.js-timezone-dropdown'); + constructor({ $dropdownEl, $inputEl, onSelectTimezone } = defaults) { + this.$dropdown = $dropdownEl; this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text'); - this.$input = $('#schedule_cron_timezone'); + this.$input = $inputEl; this.timezoneData = this.$dropdown.data('data'); + this.initDefaultTimezone(); this.initDropdown(); + + this.onSelectTimezone = onSelectTimezone; } initDropdown() { @@ -24,28 +40,12 @@ export default class TimezoneDropdown { fields: ['name'], }, clicked: cfg => this.updateInputValue(cfg), - text: item => this.formatTimezone(item), + text: item => formatTimezone(item), }); this.setDropdownToggle(); } - formatUtcOffset(offset) { - let prefix = ''; - - if (offset > 0) { - prefix = '+'; - } else if (offset < 0) { - prefix = '-'; - } - - return `${prefix} ${Math.abs(offset / 3600)}`; - } - - formatTimezone(item) { - return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`; - } - initDefaultTimezone() { const initialValue = this.$input.val(); @@ -56,13 +56,14 @@ export default class TimezoneDropdown { setDropdownToggle() { const initialValue = this.$input.val(); - this.$dropdownToggle.text(initialValue); } updateInputValue({ selectedObj, e }) { e.preventDefault(); this.$input.val(selectedObj.identifier); - gl.pipelineScheduleFieldErrors.updateFormValidityState(); + if (this.onSelectTimezone) { + this.onSelectTimezone({ selectedObj, e }); + } } } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index 4d494efef6c..dc6df27f1c7 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -41,7 +41,13 @@ export default () => { const formElement = document.getElementById('new-pipeline-schedule-form'); - gl.timezoneDropdown = new TimezoneDropdown(); + gl.timezoneDropdown = new TimezoneDropdown({ + $dropdownEl: $('.js-timezone-dropdown'), + $inputEl: $('#schedule_cron_timezone'), + onSelectTimezone: () => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }, + }); gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement); diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index 706e6ca19c3..57125c78cf6 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -50,6 +50,9 @@ export default { buttonLabel() { return this.isTodo ? MARK_TEXT : TODO_TEXT; }, + buttonTooltip() { + return !this.collapsed ? undefined : this.buttonLabel; + }, collapsedButtonIconClasses() { return this.isTodo ? 'todo-undone' : ''; }, @@ -69,7 +72,7 @@ export default { <button v-tooltip :class="buttonClasses" - :title="buttonLabel" + :title="buttonTooltip" :aria-label="buttonLabel" :data-issuable-id="issuableId" :data-issuable-type="issuableType" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 50ab7ead582..361441640e1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -109,29 +109,31 @@ export default { ></div> </div> - <div v-if="mr.isOpen" class="branch-actions d-flex"> - <a - v-if="!mr.sourceBranchRemoved" - v-tooltip - :href="webIdePath" - :title="ideButtonTitle" - :class="{ disabled: !mr.canPushToSourceBranch }" - class="btn btn-default js-web-ide d-none d-md-inline-block append-right-8" - data-placement="bottom" - tabindex="0" - role="button" - > - {{ s__('mrWidget|Open in Web IDE') }} - </a> - <button - :disabled="mr.sourceBranchRemoved" - data-target="#modal_merge_info" - data-toggle="modal" - class="btn btn-default js-check-out-branch append-right-default" - type="button" - > - {{ s__('mrWidget|Check out branch') }} - </button> + <div class="branch-actions d-flex"> + <template v-if="mr.isOpen"> + <a + v-if="!mr.sourceBranchRemoved" + v-tooltip + :href="webIdePath" + :title="ideButtonTitle" + :class="{ disabled: !mr.canPushToSourceBranch }" + class="btn btn-default js-web-ide d-none d-md-inline-block append-right-8" + data-placement="bottom" + tabindex="0" + role="button" + > + {{ s__('mrWidget|Open in Web IDE') }} + </a> + <button + :disabled="mr.sourceBranchRemoved" + data-target="#modal_merge_info" + data-toggle="modal" + class="btn btn-default js-check-out-branch append-right-default" + type="button" + > + {{ s__('mrWidget|Check out branch') }} + </button> + </template> <span class="dropdown"> <button type="button" diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue index f085ef35ccc..b25aebd7c98 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue @@ -102,7 +102,7 @@ export default { :style="{ width: onionMaxPixelWidth, height: onionMaxPixelHeight, - 'user-select': dragging === true ? 'none' : '', + 'user-select': dragging ? 'none' : null, }" class="onion-skin-frame" > diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue index 1c970b72a66..eddafc759a2 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue @@ -68,12 +68,10 @@ export default { }, startDrag() { this.dragging = true; - document.body.style.userSelect = 'none'; document.body.addEventListener('mousemove', this.dragMove); }, stopDrag() { this.dragging = false; - document.body.style.userSelect = ''; document.body.removeEventListener('mousemove', this.dragMove); }, prepareSwipe() { @@ -104,7 +102,13 @@ export default { <template> <div class="swipe view"> - <div ref="swipeFrame" class="swipe-frame"> + <div + ref="swipeFrame" + :style="{ + 'user-select': dragging ? 'none' : null, + }" + class="swipe-frame" + > <image-viewer key="swipeOldImg" ref="swipeOldImg" diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index 7207e5119ce..d9b0e4558ad 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -66,6 +66,7 @@ } } +.ci-variable-masked-item, .ci-variable-protected-item { flex: 0 1 auto; display: flex; diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss index e5edddec71e..6205ccaa52f 100644 --- a/app/assets/stylesheets/framework/system_messages.scss +++ b/app/assets/stylesheets/framework/system_messages.scss @@ -14,6 +14,7 @@ @include str-truncated(100%); margin-top: -1px; margin-bottom: 0; + font-size: $gl-font-size-small; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5d4c84c494d..7d9781ffb87 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -289,7 +289,6 @@ $gl-line-height: 16px; $gl-line-height-24: 24px; $gl-line-height-14: 14px; -$system-header-height: 35px; $issue-box-upcoming-bg: #8f8f8f; $pages-group-name-color: #4c4e54; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index a80158943c6..0c1067bfacc 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -179,6 +179,14 @@ $ide-commit-header-height: 48px; display: none; } + .monaco-editor .selected-text { + z-index: 1; + } + + .monaco-editor .view-lines { + z-index: 2; + } + .is-readonly, .editor.original { .view-lines { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ed0e9db035b..fc1c1bd9962 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -234,7 +234,7 @@ } .board-title-text { - margin-right: auto; + margin: $gl-vert-padding auto $gl-vert-padding 0; } .board-delete { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 02364180c35..54d985df9b5 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -154,11 +154,17 @@ .swipe-wrap { overflow: hidden; - border-left: 1px solid $gl-gray-400; + border-right: 1px solid $gl-gray-400; position: absolute; display: block; top: 13px; right: 7px; + + &.left-oriented { + /* only for commit view (different swipe viewer) */ + border-right: 0; + border-left: 1px solid $gl-gray-400; + } } .swipe-bar { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bcb306d97d5..792c618fd40 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -209,8 +209,7 @@ } } - .access-request-link, - .home-panel-topic-list { + .access-request-link { padding-left: $gl-padding-8; border-left: 1px solid $gl-text-color-secondary; } diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 05d88429cfe..8ef3b6502df 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,9 +7,6 @@ module IssuableActions included do before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_admin_issuable!, only: :bulk_update - before_action only: :show do - push_frontend_feature_flag(:reply_to_individual_notes, default_enabled: true) - end end def permitted_keys diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 4f641de0357..b44e3b0fff4 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -41,7 +41,7 @@ module Groups end def variable_params_attributes - %i[id key secret_value protected _destroy] + %i[id key secret_value protected masked _destroy] end def authorize_admin_build! diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index e9cd475a199..4fa6cd94ae5 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -10,6 +10,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index] + before_action only: [:metrics, :additional_metrics] do + push_frontend_feature_flag(:metrics_time_window) + end def index @environments = project.environments @@ -146,7 +149,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController def additional_metrics respond_to do |format| format.json do - additional_metrics = environment.additional_metrics || {} + additional_metrics = environment.additional_metrics(*metrics_params) || {} render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content end @@ -186,6 +189,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController @environment ||= project.environments.find(params[:id]) end + def metrics_params + return unless Feature.enabled?(:metrics_time_window, project) + return unless params[:start].present? || params[:end].present? + + params.require([:start, :end]).values_at(:start, :end) + end + def search_environment_names return [] unless params[:query] diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 6a86f8ca729..c306ba3ffcf 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -31,10 +31,7 @@ class Projects::PipelinesController < Projects::ApplicationController Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) render json: { - pipelines: PipelineSerializer - .new(project: @project, current_user: @current_user) - .with_pagination(request, response) - .represent(@pipelines, disable_coverage: true, preload: true), + pipelines: serialize_pipelines, count: { all: @pipelines_count, running: @running_count, @@ -150,6 +147,15 @@ class Projects::PipelinesController < Projects::ApplicationController private + def serialize_pipelines + ::Gitlab::GitalyClient.allow_ref_name_caching do + PipelineSerializer + .new(project: @project, current_user: @current_user) + .with_pagination(request, response) + .represent(@pipelines, disable_coverage: true, preload: true) + end + end + def render_show respond_to do |format| format.html do diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index bb658bfcc19..05a79d59ffd 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -38,6 +38,6 @@ class Projects::VariablesController < Projects::ApplicationController end def variable_params_attributes - %i[id key secret_value protected _destroy] + %i[id key secret_value protected masked _destroy] end end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index ecc34eacc7d..06d26309b5b 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -5,7 +5,6 @@ class GitlabSchema < GraphQL::Schema use Gitlab::Graphql::Authorize use Gitlab::Graphql::Present use Gitlab::Graphql::Connections - use Gitlab::Graphql::Tracing query(Types::QueryType) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 3e1bb9af5cc..d6fff1c36da 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -19,10 +19,14 @@ module BlobHelper def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) segments = [ide_path, 'project', project.full_path, 'edit', ref] - segments.concat(['-', path]) if path.present? + segments.concat(['-', encode_ide_path(path)]) if path.present? File.join(segments) end + def encode_ide_path(path) + url_encode(path).gsub('%2F', '/') + end + def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) return unless blob = readable_blob(options, path, project, ref) diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb index e3728804c2a..88ce311a1d4 100644 --- a/app/helpers/ci_variables_helper.rb +++ b/app/helpers/ci_variables_helper.rb @@ -12,4 +12,12 @@ module CiVariablesHelper ci_variable_protected_by_default? end end + + def ci_variable_masked?(variable, only_key_value) + if variable && !only_key_value + variable.masked + else + true + end + end end diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb index a29e80fe0c1..decbbbd87f2 100644 --- a/app/models/concerns/prometheus_adapter.rb +++ b/app/models/concerns/prometheus_adapter.rb @@ -51,7 +51,7 @@ module PrometheusAdapter end def build_query_args(*args) - args.map(&:id) + args.map { |arg| arg.respond_to?(:id) ? arg.id : arg } end end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 25373c7a1f7..fa29a83e517 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -170,8 +170,10 @@ class Environment < ApplicationRecord prometheus_adapter.query(:environment, self) if has_metrics? end - def additional_metrics - prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics? + def additional_metrics(*args) + return unless has_metrics? + + prometheus_adapter.query(:additional_metrics_environment, self, *args.map(&:to_f)) end # rubocop: disable CodeReuse/ServiceClass diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb index 3b6b68a9c5f..d926e39f96e 100644 --- a/app/models/individual_note_discussion.rb +++ b/app/models/individual_note_discussion.rb @@ -14,7 +14,7 @@ class IndividualNoteDiscussion < Discussion end def can_convert_to_discussion? - noteable.supports_replying_to_individual_notes? && Feature.enabled?(:reply_to_individual_notes, default_enabled: true) + noteable.supports_replying_to_individual_notes? end def convert_to_discussion!(save: false) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b780b67492f..458c57c1dc6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -858,15 +858,6 @@ class MergeRequest < ApplicationRecord end def related_notes - # Fetch comments only from last 100 commits - commits_for_notes_limit = 100 - commit_ids = commit_shas.take(commits_for_notes_limit) - - commit_notes = Note - .except(:order) - .where(project_id: [source_project_id, target_project_id]) - .for_commit_id(commit_ids) - # We're using a UNION ALL here since this results in better performance # compared to using OR statements. We're using UNION ALL since the queries # used won't produce any duplicates (e.g. a note for a commit can't also be @@ -878,6 +869,16 @@ class MergeRequest < ApplicationRecord alias_method :discussion_notes, :related_notes + def commit_notes + # Fetch comments only from last 100 commits + commit_ids = commit_shas.take(100) + + Note + .user + .where(project_id: [source_project_id, target_project_id]) + .for_commit_id(commit_ids) + end + def mergeable_discussions_state? return true unless project.only_allow_merge_if_all_discussions_are_resolved? diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 570112b63b7..d5770387872 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -2,11 +2,9 @@ class PersonalAccessToken < ApplicationRecord include Expirable - include IgnorableColumn include TokenAuthenticatable add_authentication_token_field :token, digest: true - ignore_column :token REDIS_EXPIRY_TIME = 3.minutes diff --git a/app/models/user.rb b/app/models/user.rb index e0c518a9b75..b426d100537 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -276,6 +276,7 @@ class User < ApplicationRecord scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) } scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) } scope :with_emails, -> { preload(:emails) } + scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) } # Limits the users to those that have TODOs, optionally in the given state. # diff --git a/app/serializers/group_variable_entity.rb b/app/serializers/group_variable_entity.rb index 0edab4a3092..19c5fa26f34 100644 --- a/app/serializers/group_variable_entity.rb +++ b/app/serializers/group_variable_entity.rb @@ -6,4 +6,5 @@ class GroupVariableEntity < Grape::Entity expose :value expose :protected?, as: :protected + expose :masked?, as: :masked end diff --git a/app/serializers/variable_entity.rb b/app/serializers/variable_entity.rb index 85cf367fe51..4d48e13cfca 100644 --- a/app/serializers/variable_entity.rb +++ b/app/serializers/variable_entity.rb @@ -6,4 +6,5 @@ class VariableEntity < Grape::Entity expose :value expose :protected?, as: :protected + expose :masked?, as: :masked end diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb index 318dfd4f886..9ce0fbdb206 100644 --- a/app/services/git/tag_push_service.rb +++ b/app/services/git/tag_push_service.rb @@ -13,7 +13,6 @@ module Git EventCreateService.new.push(project, current_user, push_data) Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options) - SystemHooksService.new.execute_hooks(build_system_push_data, :tag_push_hooks) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) @@ -50,17 +49,6 @@ module Git push_options: params[:push_options] || []) end - def build_system_push_data - Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - [], - '') - end - def pipeline_options {} # to be overridden in EE end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index f463e08ee7e..8ff73522e5f 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -96,14 +96,27 @@ module QuickActions end def find_labels(labels_params = nil) + extract_references(labels_params, :label) | find_labels_by_name_no_tilde(labels_params) + end + + def find_labels_by_name_no_tilde(labels_params) + return Label.none if label_with_tilde?(labels_params) + finder_params = { include_ancestor_groups: true } finder_params[:project_id] = project.id if project finder_params[:group_id] = group.id if group - finder_params[:name] = labels_params.split if labels_params + finder_params[:name] = extract_label_names(labels_params) if labels_params - result = LabelsFinder.new(current_user, finder_params).execute + LabelsFinder.new(current_user, finder_params).execute + end + + def label_with_tilde?(labels_params) + labels_params&.include?('~') + end - extract_references(labels_params, :label) | result + def extract_label_names(labels_params) + # '"A" "A B C" A B' => ["A", "A B C", "A", "B"] + labels_params.scan(/"([^"]+)"|([^ ]+)/).flatten.compact end def find_label_references(labels_param) diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml index 90c59bec975..d07cbe4589c 100644 --- a/app/views/ci/variables/_content.html.haml +++ b/app/views/ci/variables/_content.html.haml @@ -1,3 +1,3 @@ -= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want.') += _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.') = _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe = link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables') diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml index cb7779e2175..dbfa0a9e5a1 100644 --- a/app/views/ci/variables/_header.html.haml +++ b/app/views/ci/variables/_header.html.haml @@ -1,7 +1,7 @@ - expanded = local_assigns.fetch(:expanded) %h4 - = _('Environment variables') + = _('Variables') = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.btn-default.js-settings-toggle{ type: 'button' } diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index 16a7527c8ce..aecfdea10d9 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -7,12 +7,15 @@ - value = variable&.value - is_protected_default = ci_variable_protected_by_default? - is_protected = ci_variable_protected?(variable, only_key_value) +- is_masked_default = true +- is_masked = ci_variable_masked?(variable, only_key_value) - id_input_name = "#{form_field}[variables_attributes][][id]" - destroy_input_name = "#{form_field}[variables_attributes][][_destroy]" - key_input_name = "#{form_field}[variables_attributes][][key]" - value_input_name = "#{form_field}[variables_attributes][][secret_value]" - protected_input_name = "#{form_field}[variables_attributes][][protected]" +- masked_input_name = "#{form_field}[variables_attributes][][masked]" %li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } } .ci-variable-row-body @@ -22,7 +25,7 @@ name: key_input_name, value: key, placeholder: s_('CiVariables|Input variable key') } - .ci-variable-body-item + .ci-variable-body-item.gl-show-field-errors .form-control.js-secret-value-placeholder.qa-ci-variable-input-value{ class: ('hide' unless id) } = '*' * 20 %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ class: ('hide' if id), @@ -30,6 +33,7 @@ name: value_input_name, placeholder: s_('CiVariables|Input variable value') } = value + %p.masking-validation-error.gl-field-error.hide= s_("CiVariables|This variable will not be masked") - unless only_key_value .ci-variable-body-item.ci-variable-protected-item .append-right-default @@ -45,6 +49,20 @@ %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + .ci-variable-body-item.ci-variable-masked-item + .append-right-default + = s_("CiVariable|Masked") + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_masked}", + "aria-label": s_("CiVariable|Toggle masked") } + %input{ type: "hidden", + class: 'js-ci-variable-input-masked js-project-feature-toggle-input', + name: masked_input_name, + value: is_masked, + data: { default: is_masked_default.to_s } } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } = icon('minus-circle') diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 4ac5a74c85c..50adc19f524 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -12,7 +12,7 @@ = @project.name %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) - .home-panel-metadata.d-flex.align-items-center.text-secondary + .home-panel-metadata.d-flex.flex-wrap.text-secondary - if can?(current_user, :read_project, @project) %span.text-secondary = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } @@ -20,7 +20,7 @@ %span.access-request-links.prepend-left-8 = render 'shared/members/access_request_links', source: @project - if @project.tag_list.present? - %span.home-panel-topic-list.d-inline-flex.prepend-left-8 + %span.home-panel-topic-list.mt-2.w-100.d-inline-flex = sprite_icon('tag', size: 16, css_class: 'icon append-right-4') - @project.topics_to_show.each do |topic| diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml index 6dffc7c4390..70521ed892e 100644 --- a/app/views/projects/diffs/_replaced_image_diff.html.haml +++ b/app/views/projects/diffs/_replaced_image_diff.html.haml @@ -37,7 +37,7 @@ .swipe-frame .frame.deleted = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false) - .swipe-wrap + .swipe-wrap.left-oriented = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path } %span.swipe-bar %span.top-handle diff --git a/changelogs/unreleased/13784-validate-variables-for-masking.yml b/changelogs/unreleased/13784-validate-variables-for-masking.yml new file mode 100644 index 00000000000..e8e97fac3d2 --- /dev/null +++ b/changelogs/unreleased/13784-validate-variables-for-masking.yml @@ -0,0 +1,5 @@ +--- +title: Add control for masking variable values in runner logs +merge_request: 26751 +author: +type: added diff --git a/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml b/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml new file mode 100644 index 00000000000..5942860a20f --- /dev/null +++ b/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml @@ -0,0 +1,5 @@ +--- +title: Show download diff links for closed MRs +merge_request: 26772 +author: +type: changed diff --git a/changelogs/unreleased/47234-composable-auto-devops.yml b/changelogs/unreleased/47234-composable-auto-devops.yml new file mode 100644 index 00000000000..9403c5ae6d3 --- /dev/null +++ b/changelogs/unreleased/47234-composable-auto-devops.yml @@ -0,0 +1,5 @@ +--- +title: Split Auto-DevOps.gitlab-ci.yml into reusable templates +merge_request: 26520 +author: +type: changed diff --git a/changelogs/unreleased/47771-highlighting-in-diff.yml b/changelogs/unreleased/47771-highlighting-in-diff.yml new file mode 100644 index 00000000000..a8e8cbf0174 --- /dev/null +++ b/changelogs/unreleased/47771-highlighting-in-diff.yml @@ -0,0 +1,5 @@ +--- +title: Enabled text selection highlighting in diffs in Web IDE +merge_request: 26721 +author: Isaac Smith +type: fixed diff --git a/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml b/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml new file mode 100644 index 00000000000..b8d58d6bd30 --- /dev/null +++ b/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml @@ -0,0 +1,5 @@ +--- +title: Only execute system hooks once when pushing tags +merge_request: 26888 +author: +type: fixed diff --git a/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml b/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml new file mode 100644 index 00000000000..30d8c0e95d7 --- /dev/null +++ b/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml @@ -0,0 +1,5 @@ +--- +title: Fix quick actions add label name middle word overlaps +merge_request: 26602 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml b/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml new file mode 100644 index 00000000000..7af4739136b --- /dev/null +++ b/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml @@ -0,0 +1,5 @@ +--- +title: Exclude system notes from commits in merge request discussions +merge_request: 26396 +author: +type: fixed diff --git a/changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml b/changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml new file mode 100644 index 00000000000..4c16b635297 --- /dev/null +++ b/changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml @@ -0,0 +1,5 @@ +--- +title: Remove non-functional add issue button on welcome list +merge_request: !26742 +author: +type: fixed diff --git a/changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml b/changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml new file mode 100644 index 00000000000..688190f4458 --- /dev/null +++ b/changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml @@ -0,0 +1,5 @@ +--- +title: Update system message banner font size to 12px +merge_request: 26293 +author: +type: changed diff --git a/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml b/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml new file mode 100644 index 00000000000..42bc320a542 --- /dev/null +++ b/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Extend timezone dropdown +merge_request: 26311 +author: +type: changed diff --git a/changelogs/unreleased/feature-webide_escaping.yml b/changelogs/unreleased/feature-webide_escaping.yml new file mode 100644 index 00000000000..88fa1bd948e --- /dev/null +++ b/changelogs/unreleased/feature-webide_escaping.yml @@ -0,0 +1,5 @@ +--- +title: Fixed bug with hashes in urls in WebIDE +merge_request: 54376 +author: Kieran Andrews +type: fixed diff --git a/changelogs/unreleased/fix-issues-time-counter.yml b/changelogs/unreleased/fix-issues-time-counter.yml new file mode 100644 index 00000000000..76f17063db5 --- /dev/null +++ b/changelogs/unreleased/fix-issues-time-counter.yml @@ -0,0 +1,5 @@ +--- +title: Make time counters show 'just now' for everything under one minute +merge_request: 25992 +author: Sergiu Marton +type: changed diff --git a/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml b/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml new file mode 100644 index 00000000000..770186a64b0 --- /dev/null +++ b/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml @@ -0,0 +1,5 @@ +--- +title: Fixed duplicated diff too large error message +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/graphql-prometheus.yml b/changelogs/unreleased/graphql-prometheus.yml deleted file mode 100644 index 180577f3aec..00000000000 --- a/changelogs/unreleased/graphql-prometheus.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added prometheus monitoring to GraphQL -merge_request: -author: -type: added diff --git a/changelogs/unreleased/localize-notification-dropdown.yml b/changelogs/unreleased/localize-notification-dropdown.yml new file mode 100644 index 00000000000..9599aaf344b --- /dev/null +++ b/changelogs/unreleased/localize-notification-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Localize notifications dropdown +merge_request: 26844 +author: +type: changed diff --git a/changelogs/unreleased/recreate-all-diffs-on-import.yml b/changelogs/unreleased/recreate-all-diffs-on-import.yml deleted file mode 100644 index fd9124372f3..00000000000 --- a/changelogs/unreleased/recreate-all-diffs-on-import.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Force to recreate all MR diffs on import -merge_request: 26480 -author: -type: fixed diff --git a/changelogs/unreleased/sh-cache-pipeline-find-commits.yml b/changelogs/unreleased/sh-cache-pipeline-find-commits.yml new file mode 100644 index 00000000000..2acf180d6fa --- /dev/null +++ b/changelogs/unreleased/sh-cache-pipeline-find-commits.yml @@ -0,0 +1,5 @@ +--- +title: Cache FindCommit results in pipelines view +merge_request: 26776 +author: +type: performance diff --git a/changelogs/unreleased/sh-fix-project-branches-merge-status.yml b/changelogs/unreleased/sh-fix-project-branches-merge-status.yml deleted file mode 100644 index 65f41b3faf9..00000000000 --- a/changelogs/unreleased/sh-fix-project-branches-merge-status.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix API /project/:id/branches not returning correct merge status -merge_request: 26785 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-rugged-tree-entries.yml b/changelogs/unreleased/sh-fix-rugged-tree-entries.yml deleted file mode 100644 index 97b27678905..00000000000 --- a/changelogs/unreleased/sh-fix-rugged-tree-entries.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Avoid excessive recursive calls with Rugged TreeEntries -merge_request: 26813 -author: -type: fixed diff --git a/changelogs/unreleased/sh-force-gc-after-import.yml b/changelogs/unreleased/sh-force-gc-after-import.yml deleted file mode 100644 index 755d66c1607..00000000000 --- a/changelogs/unreleased/sh-force-gc-after-import.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Force a full GC after importing a project -merge_request: 26803 -author: -type: performance diff --git a/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml b/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml new file mode 100644 index 00000000000..124584c9bd4 --- /dev/null +++ b/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml @@ -0,0 +1,5 @@ +--- +title: Move project tags to separate line +merge_request: 26797 +author: +type: other diff --git a/danger/gitlab_ui_wg/Dangerfile b/danger/gitlab_ui_wg/Dangerfile new file mode 100644 index 00000000000..02d94fa5ab7 --- /dev/null +++ b/danger/gitlab_ui_wg/Dangerfile @@ -0,0 +1,55 @@ +def mention_single_codebase_approvers + frontend_maintainers = %w(@filipa @iamphill @psimyn @sarahghp @mishunov) + ux_maintainers = %w(@tauriedavis @rverissimo) + + rows = [] + users = [] + + if gitlab.mr_labels.include?('frontend') + frontend_maintainer = frontend_maintainers.sample + + rows << "| ~frontend | `#{frontend_maintainer}`" + users << frontend_maintainer + end + + if gitlab.mr_labels.include?('UX') + ux_maintainers = ux_maintainers.sample + + rows << "| ~UX | `#{ux_maintainers}`" + users << ux_maintainers + end + + if rows.empty? + backup_maintainer = frontend_maintainers.sample + + rows << "| ~frontend / ~UX | `#{backup_maintainer}`" + users << backup_maintainer + end + + markdown(<<~MARKDOWN.strip) + ## GitLab UI Working Group changes + + This merge request contains changes related to the work of [cleaning up CSS and creating + reusable components](https://gitlab.com/groups/gitlab-org/-/epics/950). + These changes will need to be reviewed and approved by the following engineers: + + | Category | Reviewer + |----------|--------- + #{rows.join("\n")} + + To make sure this happens, please follow these steps: + + 1. Add all of the mentioned users to the list of merge request approvals. + 2. Assign the merge request to the first person in the above list. + + If you are a reviewer, please follow these steps: + + 1. Review the merge request. If it is good to go, approve it. + 2. Once approved, assign to the next person in the above list. If you are + the last person in the list, merge the merge request. + MARKDOWN +end + +if gitlab.mr_labels.include?('CSS cleanup') + mention_single_codebase_approvers +end diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 808bc96a0a0..3a4625c4eb6 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -60,7 +60,9 @@ categories = changes.keys - [:unknown] # Single codebase MRs are reviewed using a slightly different process, so we # disable the review roulette for such MRs. -if changes.any? && !gitlab.mr_labels.include?('single codebase') +# CSS Clean up MRs are reviewed using a slightly different process, so we +# disable the review roulette for such MRs. +if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_labels.include?('CSS cleanup') team = begin helper.project_team diff --git a/doc/administration/auth/google_secure_ldap.md b/doc/administration/auth/google_secure_ldap.md new file mode 100644 index 00000000000..65a51fc4aa0 --- /dev/null +++ b/doc/administration/auth/google_secure_ldap.md @@ -0,0 +1,207 @@ +# Google Secure LDAP **[CORE ONLY]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46391) in GitLab 11.9. + +[Google Cloud Identity](https://cloud.google.com/identity/) provides a Secure +LDAP service that can be configured with GitLab for authentication and group sync. + +Secure LDAP requires a slightly different configuration than standard LDAP servers. +The steps below cover: + +- Configuring the Secure LDAP Client in the Google Admin console. +- Required GitLab configuration. + +## Configuring Google LDAP client + +1. Navigate to https://admin.google.com and sign in as a GSuite domain administrator. + +1. Go to **Apps > LDAP > Add Client**. + +1. Provide an `LDAP client name` and an optional `Description`. Any descriptive + values are acceptable. For example, the name could be 'GitLab' and the + description could be 'GitLab LDAP Client'. Click the **Continue** button. + + ![Add LDAP Client Step 1](img/google_secure_ldap_add_step_1.png) + +1. Set **Access Permission** according to your needs. You must choose either + 'Entire domain (GitLab)' or 'Selected organizational units' for both 'Verify user + credentials' and 'Read user information'. Select 'Add LDAP Client' + + TIP: **Tip:** If you plan to use GitLab [LDAP Group Sync](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync) + , turn on 'Read group information'. + + ![Add LDAP Client Step 2](img/google_secure_ldap_add_step_2.png) + +1. Download the generated certificate. This is required for GitLab to + communicate with the Google Secure LDAP service. Save the downloaded certificates + for later use. After downloading, click the **Continue to Client Details** button. + +1. Expand the **Service Status** section and turn the LDAP client 'ON for everyone'. + After selecting 'Save', click on the 'Service Status' bar again to collapse + and return to the rest of the settings. + +1. Expand the **Authentication** section and choose 'Generate New Credentials'. + Copy/note these credentials for later use. After selecting 'Close', click + on the 'Authentication' bar again to collapse and return to the rest of the settings. + +Now the Google Secure LDAP Client configuration is finished. The screenshot below +shows an example of the final settings. Continue on to configure GitLab. + +![LDAP Client Settings](img/google_secure_ldap_client_settings.png) + +## Configuring GitLab + +Edit GitLab configuration, inserting the access credentials and certificate +obtained earlier. + +The following are the configuration keys that need to be modified using the +values obtained during the LDAP client configuration earlier: + +- `bind_dn`: The access credentials username +- `password`: The access credentials password +- `cert`: The `.crt` file text from the downloaded certificate bundle +- `key`: The `.key` file text from the downloaded certificate bundle + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['ldap_enabled'] = true + gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below + main: # 'main' is the GitLab 'provider ID' of this LDAP server + label: 'Google Secure LDAP' + + host: 'ldap.google.com' + port: 636 + uid: 'uid' + bind_dn: 'DizzyHorse' + password: 'd6V5H8nhMUW9AuDP25abXeLd' + encryption: 'simple_tls' + verify_certificates: true + + tls_options: + cert: | + -----BEGIN CERTIFICATE----- + MIIDbDCCAlSgAwIBAgIGAWlzxiIfMA0GCSqGSIb3DQEBCwUAMHcxFDASBgNVBAoTC0dvb2dsZSBJ + bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UE + CxMGR1N1aXRlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xOTAzMTIyMTE5 + MThaFw0yMjAzMTEyMTE5MThaMHcxFDASBgNVBAoTC0dvb2dsZSBJbmMuMRYwFAYDVQQHEw1Nb3Vu + dGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UECxMGR1N1aXRlMQswCQYDVQQG + EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB + ALOTy4aC38dyjESk6N8fRsKk8DN23ZX/GaNFL5OUmmA1KWzrvVC881OzNdtGm3vNOIxr9clteEG/ + tQwsmsJvQT5U+GkBt+tGKF/zm7zueHUYqTP7Pg5pxAnAei90qkIRFi17ulObyRHPYv1BbCt8pxNB + 4fG/gAXkFbCNxwh1eiQXXRTfruasCZ4/mHfX7MVm8JmWU9uAVIOLW+DSWOFhrDQduJdGBXJOyC2r + Gqoeg9+tkBmNH/jjxpnEkFW8q7io9DdOUqqNgoidA1h9vpKTs3084sy2DOgUvKN9uXWx14uxIyYU + Y1DnDy0wczcsuRt7l+EgtCEgpsLiLJQbKW+JS1UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAf60J + yazhbHkDKIH2gFxfm7QLhhnqsmafvl4WP7JqZt0u0KdnvbDPfokdkM87yfbKJU1MTI86M36wEC+1 + P6bzklKz7kXbzAD4GggksAzxsEE64OWHC+Y64Tkxq2NiZTw/76POkcg9StiIXjG0ZcebHub9+Ux/ + rTncip92nDuvgEM7lbPFKRIS/YMhLCk09B/U0F6XLsf1yYjyf5miUTDikPkov23b/YGfpc8kh6hq + 1kqdi6a1cYPP34eAhtRhMqcZU9qezpJF6s9EeN/3YFfKzLODFSsVToBRAdZgGHzj//SAtLyQTD4n + KCSvK1UmaMxNaZyTHg8JnMf0ZuRpv26iSg== + -----END CERTIFICATE----- + + key: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzk8uGgt/HcoxEpOjfH0bCpPAz + dt2V/xmjRS+TlJpgNSls671QvPNTszXbRpt7zTiMa/XJbXhBv7UMLJrCb0E+VPhpAbfrRihf85u8 + 7nh1GKkz+z4OacQJwHovdKpCERYte7pTm8kRz2L9QWwrfKcTQeHxv4AF5BWwjccIdXokF10U367m + rAmeP5h31+zFZvCZllPbgFSDi1vg0ljhYaw0HbiXRgVyTsgtqxqqHoPfrZAZjR/448aZxJBVvKu4 + qPQ3TlKqjYKInQNYfb6Sk7N9POLMtgzoFLyjfbl1sdeLsSMmFGNQ5w8tMHM3LLkbe5fhILQhIKbC + 4iyUGylviUtVAgMBAAECggEAIPb0CQy0RJoX+q/lGbRVmnyJpYDf+115WNnl+mrwjdGkeZyqw4v0 + BPzkWYzUFP1esJRO6buBNFybQRFdFW0z5lvVv/zzRKq71aVUBPInxaMRyHuJ8D5lIL8nDtgVOwyE + 7DOGyDtURUMzMjdUwoTe7K+O6QBU4X/1pVPZYgmissYSMmt68LiP8k0p601F4+r5xOi/QEy44aVp + aOJZBUOisKB8BmUXZqmQ4Cy05vU9Xi1rLyzkn9s7fxnZ+JO6Sd1r0Thm1mE0yuPgxkDBh/b4f3/2 + GsQNKKKCiij/6TfkjnBi8ZvWR44LnKpu760g/K7psVNrKwqJG6C/8RAcgISWQQKBgQDop7BaKGhK + 1QMJJ/vnlyYFTucfGLn6bM//pzTys5Gop0tpcfX/Hf6a6Dd+zBhmC3tBmhr80XOX/PiyAIbc0lOI + 31rafZuD/oVx5mlIySWX35EqS14LXmdVs/5vOhsInNgNiE+EPFf1L9YZgG/zA7OUBmqtTeYIPDVC + 7ViJcydItQKBgQDFmK0H0IA6W4opGQo+zQKhefooqZ+RDk9IIZMPOAtnvOM7y3rSVrfsSjzYVuMS + w/RP/vs7rwhaZejnCZ8/7uIqwg4sdUBRzZYR3PRNFeheW+BPZvb+2keRCGzOs7xkbF1mu54qtYTa + HZGZj1OsD83AoMwVLcdLDgO1kw32dkS8IQKBgFRdgoifAHqqVah7VFB9se7Y1tyi5cXWsXI+Wufr + j9U9nQ4GojK52LqpnH4hWnOelDqMvF6TQTyLIk/B+yWWK26Ft/dk9wDdSdystd8L+dLh4k0Y+Whb + +lLMq2YABw+PeJUnqdYE38xsZVHoDjBsVjFGRmbDybeQxauYT7PACy3FAoGBAK2+k9bdNQMbXp7I + j8OszHVkJdz/WXlY1cmdDAxDwXOUGVKIlxTAf7TbiijILZ5gg0Cb+hj+zR9/oI0WXtr+mAv02jWp + W8cSOLS4TnBBpTLjIpdu+BwbnvYeLF6MmEjNKEufCXKQbaLEgTQ/XNlchBSuzwSIXkbWqdhM1+gx + EjtBAoGARAdMIiDMPWIIZg3nNnFebbmtBP0qiBsYohQZ+6i/8s/vautEHBEN6Q0brIU/goo+nTHc + t9VaOkzjCmAJSLPUanuBC8pdYgLu5J20NXUZLD9AE/2bBT3OpezKcdYeI2jqoc1qlWHlNtVtdqQ2 + AcZSFJQjdg5BTyvdEDhaYUKGdRw= + -----END PRIVATE KEY----- + EOS + ``` + +1. Save the file and [reconfigure] GitLab for the changes to take effect. + +--- + +**For installations from source** + +1. Edit `config/gitlab.yml`: + + ```yaml + ldap: + enabled: true + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + label: 'Google Secure LDAP' + + host: 'ldap.google.com' + port: 636 + uid: 'uid' + bind_dn: 'DizzyHorse' + password: 'd6V5H8nhMUW9AuDP25abXeLd' + encryption: 'simple_tls' + verify_certificates: true + + tls_options: + cert: | + -----BEGIN CERTIFICATE----- + MIIDbDCCAlSgAwIBAgIGAWlzxiIfMA0GCSqGSIb3DQEBCwUAMHcxFDASBgNVBAoTC0dvb2dsZSBJ + bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UE + CxMGR1N1aXRlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xOTAzMTIyMTE5 + MThaFw0yMjAzMTEyMTE5MThaMHcxFDASBgNVBAoTC0dvb2dsZSBJbmMuMRYwFAYDVQQHEw1Nb3Vu + dGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UECxMGR1N1aXRlMQswCQYDVQQG + EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB + ALOTy4aC38dyjESk6N8fRsKk8DN23ZX/GaNFL5OUmmA1KWzrvVC881OzNdtGm3vNOIxr9clteEG/ + tQwsmsJvQT5U+GkBt+tGKF/zm7zueHUYqTP7Pg5pxAnAei90qkIRFi17ulObyRHPYv1BbCt8pxNB + 4fG/gAXkFbCNxwh1eiQXXRTfruasCZ4/mHfX7MVm8JmWU9uAVIOLW+DSWOFhrDQduJdGBXJOyC2r + Gqoeg9+tkBmNH/jjxpnEkFW8q7io9DdOUqqNgoidA1h9vpKTs3084sy2DOgUvKN9uXWx14uxIyYU + Y1DnDy0wczcsuRt7l+EgtCEgpsLiLJQbKW+JS1UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAf60J + yazhbHkDKIH2gFxfm7QLhhnqsmafvl4WP7JqZt0u0KdnvbDPfokdkM87yfbKJU1MTI86M36wEC+1 + P6bzklKz7kXbzAD4GggksAzxsEE64OWHC+Y64Tkxq2NiZTw/76POkcg9StiIXjG0ZcebHub9+Ux/ + rTncip92nDuvgEM7lbPFKRIS/YMhLCk09B/U0F6XLsf1yYjyf5miUTDikPkov23b/YGfpc8kh6hq + 1kqdi6a1cYPP34eAhtRhMqcZU9qezpJF6s9EeN/3YFfKzLODFSsVToBRAdZgGHzj//SAtLyQTD4n + KCSvK1UmaMxNaZyTHg8JnMf0ZuRpv26iSg== + -----END CERTIFICATE----- + + key: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzk8uGgt/HcoxEpOjfH0bCpPAz + dt2V/xmjRS+TlJpgNSls671QvPNTszXbRpt7zTiMa/XJbXhBv7UMLJrCb0E+VPhpAbfrRihf85u8 + 7nh1GKkz+z4OacQJwHovdKpCERYte7pTm8kRz2L9QWwrfKcTQeHxv4AF5BWwjccIdXokF10U367m + rAmeP5h31+zFZvCZllPbgFSDi1vg0ljhYaw0HbiXRgVyTsgtqxqqHoPfrZAZjR/448aZxJBVvKu4 + qPQ3TlKqjYKInQNYfb6Sk7N9POLMtgzoFLyjfbl1sdeLsSMmFGNQ5w8tMHM3LLkbe5fhILQhIKbC + 4iyUGylviUtVAgMBAAECggEAIPb0CQy0RJoX+q/lGbRVmnyJpYDf+115WNnl+mrwjdGkeZyqw4v0 + BPzkWYzUFP1esJRO6buBNFybQRFdFW0z5lvVv/zzRKq71aVUBPInxaMRyHuJ8D5lIL8nDtgVOwyE + 7DOGyDtURUMzMjdUwoTe7K+O6QBU4X/1pVPZYgmissYSMmt68LiP8k0p601F4+r5xOi/QEy44aVp + aOJZBUOisKB8BmUXZqmQ4Cy05vU9Xi1rLyzkn9s7fxnZ+JO6Sd1r0Thm1mE0yuPgxkDBh/b4f3/2 + GsQNKKKCiij/6TfkjnBi8ZvWR44LnKpu760g/K7psVNrKwqJG6C/8RAcgISWQQKBgQDop7BaKGhK + 1QMJJ/vnlyYFTucfGLn6bM//pzTys5Gop0tpcfX/Hf6a6Dd+zBhmC3tBmhr80XOX/PiyAIbc0lOI + 31rafZuD/oVx5mlIySWX35EqS14LXmdVs/5vOhsInNgNiE+EPFf1L9YZgG/zA7OUBmqtTeYIPDVC + 7ViJcydItQKBgQDFmK0H0IA6W4opGQo+zQKhefooqZ+RDk9IIZMPOAtnvOM7y3rSVrfsSjzYVuMS + w/RP/vs7rwhaZejnCZ8/7uIqwg4sdUBRzZYR3PRNFeheW+BPZvb+2keRCGzOs7xkbF1mu54qtYTa + HZGZj1OsD83AoMwVLcdLDgO1kw32dkS8IQKBgFRdgoifAHqqVah7VFB9se7Y1tyi5cXWsXI+Wufr + j9U9nQ4GojK52LqpnH4hWnOelDqMvF6TQTyLIk/B+yWWK26Ft/dk9wDdSdystd8L+dLh4k0Y+Whb + +lLMq2YABw+PeJUnqdYE38xsZVHoDjBsVjFGRmbDybeQxauYT7PACy3FAoGBAK2+k9bdNQMbXp7I + j8OszHVkJdz/WXlY1cmdDAxDwXOUGVKIlxTAf7TbiijILZ5gg0Cb+hj+zR9/oI0WXtr+mAv02jWp + W8cSOLS4TnBBpTLjIpdu+BwbnvYeLF6MmEjNKEufCXKQbaLEgTQ/XNlchBSuzwSIXkbWqdhM1+gx + EjtBAoGARAdMIiDMPWIIZg3nNnFebbmtBP0qiBsYohQZ+6i/8s/vautEHBEN6Q0brIU/goo+nTHc + t9VaOkzjCmAJSLPUanuBC8pdYgLu5J20NXUZLD9AE/2bBT3OpezKcdYeI2jqoc1qlWHlNtVtdqQ2 + AcZSFJQjdg5BTyvdEDhaYUKGdRw= + -----END PRIVATE KEY----- + ``` + +1. Save the file and [restart] GitLab for the changes to take effect. + + +[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../restart_gitlab.md#installations-from-source diff --git a/doc/administration/auth/img/google_secure_ldap_add_step_1.png b/doc/administration/auth/img/google_secure_ldap_add_step_1.png Binary files differnew file mode 100644 index 00000000000..fd254443d75 --- /dev/null +++ b/doc/administration/auth/img/google_secure_ldap_add_step_1.png diff --git a/doc/administration/auth/img/google_secure_ldap_add_step_2.png b/doc/administration/auth/img/google_secure_ldap_add_step_2.png Binary files differnew file mode 100644 index 00000000000..611a21ae03c --- /dev/null +++ b/doc/administration/auth/img/google_secure_ldap_add_step_2.png diff --git a/doc/administration/auth/img/google_secure_ldap_client_settings.png b/doc/administration/auth/img/google_secure_ldap_client_settings.png Binary files differnew file mode 100644 index 00000000000..3c0b3f3d4bd --- /dev/null +++ b/doc/administration/auth/img/google_secure_ldap_client_settings.png diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 440c2b1285a..2d057dc7509 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -48,6 +48,14 @@ LDAP-enabled users can always authenticate with Git using their GitLab username or email and LDAP password, even if password authentication for Git is disabled in the application settings. +## Google Secure LDAP **[CORE ONLY]** + +> Introduced in GitLab 11.9. + +[Google Cloud Identity](https://cloud.google.com/identity/) provides a Secure +LDAP service that can be configured with GitLab for authentication and group sync. +See [Google Secure LDAP](google_secure_ldap.md) for detailed configuration instructions. + ## Configuration NOTE: **Note**: diff --git a/doc/api/runners.md b/doc/api/runners.md index 7d7215e6b80..0b7ef46888c 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -256,6 +256,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git ## List runner's jobs +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15432) in GitLab 10.3. + List jobs that are being processed or were processed by specified Runner. ``` diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 9dbcf9d1155..53651a807c2 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -81,7 +81,7 @@ The process of adding Review Apps in your workflow is as follows: 1. Set up the infrastructure to host and deploy the Review Apps. 1. [Install](https://docs.gitlab.com/runner/install/) and [configure](https://docs.gitlab.com/runner/commands/) a Runner to do deployment. -1. Set up a job in `.gitlab-ci.yml` that uses the predefined [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches. +1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches. 1. Optionally, set a job that [manually stops](../environments.md#stopping-an-environment) the Review Apps. After adding Review Apps to your workflow, you follow the branched Git flow. That is: diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 12b027e7d79..592fdfd2873 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -2,49 +2,244 @@ table_display_block: true --- -# GitLab CI/CD Variables +# GitLab CI/CD environment variables +{: #variables} -When receiving a job from GitLab CI, the [Runner](https://docs.gitlab.com/runner/) prepares the build environment. -It starts by setting a list of: +After a brief overview over the use of environment +variables, this document teaches you how to use GitLab CI/CD's +variables, presents the full reference for predefined variables, +and dives into more advanced applications. -- [Predefined environment variables](#predefined-environment-variables). -- Other variables. +## Overview -## Priority of variables +An environment variable is a dynamic-named value that can +affect the way running processes will behave on an operating +system. -Variables of different types can take precedence over other variables, depending on where they are defined. +They are part of the environment in which a process runs. +For example, a running process can query the value of the +`TEMP` environment variable to discover a suitable location +to store temporary files, or to define a `URL` for a database +that can be reused in different scripts. -The order of precedence for variables is (from highest to lowest): +Variables are useful for customizing your jobs in GitLab +CI/CD's pipelines. Using variables means no hardcoded values. -1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables). -1. Project-level [variables](#variables) or [protected variables](#protected-variables). -1. Group-level [variables](#variables) or [protected variables](#protected-variables). -1. YAML-defined [job-level variables](../yaml/README.md#variables). -1. YAML-defined [global variables](../yaml/README.md#variables). -1. [Deployment variables](#deployment-variables). -1. [Predefined environment variables](#predefined-environment-variables). +### Predefined environment variables -For example, you define: +GitLab CI/CD has a default set of +[predefined variables](predefined_variables.md) +which can be used without any specification needed. +You can call issues numbers, user names, branch names, +pipeline and commit IDs, and much more. -- `API_TOKEN=secure` as a project variable. -- `API_TOKEN=yaml` in your `.gitlab-ci.yml`. +Predefined environment variables are the ones that GitLab +provides out of the box for the local environment of the Runner. -`API_TOKEN` will take the value `secure` as the project variables take precedence over those defined -in `.gitlab-ci.yml`. +GitLab reads the .gitlab-ci.yml file, sends the information +to the Runner (which runs the script commands), under which +the variables are exposed. -## Unsupported variables +For example, two jobs under the same pipeline can share the same +`CI_PIPELINE_ID` variable, but each one has its own `CI_JOB_ID` +variable. -There are cases where some variables cannot be used in the context of a -`.gitlab-ci.yml` definition (for example under `script`). Read more -about which variables are [not supported](where_variables_can_be_used.md). +### Custom environment variables + +When your use case requires a specific variable, you can +[set them up easily from the UI](#creating-a-custom-environment-variable) +or directly in the `.gitlab-ci.yml` file and reuse them as you wish. + +That can be very powerful as it can be used for scripting without +the need to specify the value itself. + +## Getting started + +To get started with environment variables in the scope of GitLab +CI/CD, let's go over a few examples. + +### Using predefined environment variables + +To get started, choose one of the existing +[predefined variables](predefined_variables.md) +to be output by the Runner. For example, let's say that you want +a given job you're running through your script to output the +stage that job is running for. In your `.gitlab-ci.yml` file, +call the variable from your script according to the [syntaxes](#syntax-of-variables-in-job-scripts) available. To +output the job stage, use the predefined variable `CI_JOB_STAGE`: + +```yaml +test_variable: + stage: test + script: + - echo $CI_JOB_STAGE +``` + +For this case, the Runner will output the `stage` for the +job `test_variable`, which is `test`: + +![Output `$CI_JOB_STAGE`](img/ci_job_stage_output_example.png) + +As another example, let's say you're using your own GitLab +instance you want to know what domain your GitLab Pages are +served under. You can easily call it with the predefined +variable `$CI_PAGES_DOMAIN` in your script: + +```yaml +pages: + script: + - ... + - echo $CI_PAGES_DOMAIN +``` + +For GitLab.com users, the output will be `gitlab.io`. For your +private instance, the output will be whatever your sysadmin has +defined. + +### Creating a custom environment variable + +Assume you have something you want to repeat through your scripts +in GitLab CI/CD's configuration file. To keep this example simple, +let's say you want to output `HELLO WORLD` for a `TEST` variable. + +You can either set the variable directly in the `.gitlab-ci.yml` +file or through the UI. + +#### Via [`.gitlab-ci.yml`](../yaml/README.md#variables) + +```yaml +variables: + TEST: "HELLO WORLD" +``` + +For a deeper look into them, see [`.gitlab-ci.yml` defined variables](#gitlab-ciyml-defined-variables). + +#### Via the UI + +From the UI, navigate to your project's **Settings > CI/CD** and +expand **Environment variables**. Create a new variable by naming +it in the field **Input variable key**, and define its value in the +**Input variable value** field: + +![CI/CD settings - new variable](img/new_custom_variable_example.png) + +Once you've set the variables, call them from the `.gitlab-ci.yml` file: + +```yaml +test_variable: + stage: test + script: + - echo $CI_JOB_STAGE # calls a predefined variable + - echo $TEST # calls a custom variable +``` + +The output will be: + +![Output custom variable](img/custom_variable_output.png) + +CAUTION: **Important:** +Be aware that variables are not masked, and their values can be shown +in the job logs if explicitly asked to do so. If your project is public or +internal, you can set the pipelines private from your [project's Pipelines +settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). +Follow the discussion in issue [#13784][ce-13784] for masking the variables. + +### Syntax of environment variables in job scripts +{: #syntax-of-variables-in-job-scripts} -## Predefined environment variables +All variables are set as environment variables in the build environment, and +they are accessible with normal methods that are used to access such variables. +In most cases `bash` or `sh` is used to execute the job script. + +To access environment variables, use the syntax for your Runner's [shell][shellexecutors]. + +| Shell | Usage | +|----------------------|-----------------| +| bash/sh | `$variable` | +| windows batch | `%variable%` | +| PowerShell | `$env:variable` | + +To access environment variables in bash, prefix the variable name with (`$`): + +```yaml +job_name: + script: + - echo $CI_JOB_ID +``` + +To access environment variables in **Windows Batch**, surround the variable +with (`%`): + +```yaml +job_name: + script: + - echo %CI_JOB_ID% +``` -See [full reference of predefined environment variables](predefined_variables.md). +To access environment variables in a **Windows PowerShell** environment, prefix +the variable name with (`$env:`): + +```yaml +job_name: + script: + - echo $env:CI_JOB_ID +``` + +You can also list all environment variables with the `export` command, +but be aware that this will also expose the values of all the variables +you set, in the job log: + +```yaml +job_name: + script: + - export +``` + +Example values: + +```bash +export CI_JOB_ID="50" +export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a" +export CI_COMMIT_SHORT_SHA="1ecfd275" +export CI_COMMIT_REF_NAME="master" +export CI_REPOSITORY_URL="https://gitlab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" +export CI_COMMIT_TAG="1.0.0" +export CI_JOB_NAME="spec:other" +export CI_JOB_STAGE="test" +export CI_JOB_MANUAL="true" +export CI_JOB_TRIGGERED="true" +export CI_JOB_TOKEN="abcde-1234ABCD5678ef" +export CI_PIPELINE_ID="1000" +export CI_PIPELINE_IID="10" +export CI_PAGES_DOMAIN="gitlab.io" +export CI_PAGES_URL="https://gitlab-org.gitlab.io/gitlab-ce" +export CI_PROJECT_ID="34" +export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" +export CI_PROJECT_NAME="gitlab-ce" +export CI_PROJECT_NAMESPACE="gitlab-org" +export CI_PROJECT_PATH="gitlab-org/gitlab-ce" +export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce" +export CI_REGISTRY="registry.example.com" +export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce" +export CI_RUNNER_ID="10" +export CI_RUNNER_DESCRIPTION="my runner" +export CI_RUNNER_TAGS="docker, linux" +export CI_SERVER="yes" +export CI_SERVER_NAME="GitLab" +export CI_SERVER_REVISION="70606bf" +export CI_SERVER_VERSION="8.9.0" +export CI_SERVER_VERSION_MAJOR="8" +export CI_SERVER_VERSION_MINOR="9" +export CI_SERVER_VERSION_PATCH="0" +export GITLAB_USER_ID="42" +export GITLAB_USER_EMAIL="user@example.com" +export CI_REGISTRY_USER="gitlab-ci-token" +export CI_REGISTRY_PASSWORD="longalfanumstring" +``` -## `.gitlab-ci.yml` defined variables +### `.gitlab-ci.yml` defined variables -NOTE **Note:** +NOTE: **Note:** This feature requires GitLab Runner 0.5.0 or higher and GitLab 7.14 or higher. GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in the @@ -82,42 +277,63 @@ script: - 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR' ``` -## Variables +### Group-level environment variables +{: #group-level-variables} -> Group-level variables were introduced in GitLab 9.4. - -CAUTION: **Important:** -Be aware that variables are not masked, and their values can be shown -in the job logs if explicitly asked to do so. If your project is public or -internal, you can set the pipelines private from your [project's Pipelines -settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). -Follow the discussion in issue [#13784][ce-13784] for masking the variables. +> Introduced in GitLab 9.4. -GitLab CI allows you to define per-project or per-group variables -that are set in the pipeline environment. The variables are stored out of +GitLab CI/CD allows you to define per-project or per-group variables +that are set in the pipeline environment. Group-level variables are stored out of the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner -making them available during a pipeline run. It's the recommended method to +making them available during a pipeline run. It's the **recommended method** to use for storing things like passwords, SSH keys, and credentials. -Project-level variables can be added by: +Group-level variables can be added by: -1. Navigating to your project's **Settings > CI/CD** page. +1. Navigating to your group's **Settings > CI/CD** page. 1. Inputing variable keys and values in the **Environment variables** section. +Any variables of [subgroups](../../user/group/subgroups/index.md) will be inherited recursively. -Group-level variables can be added by: +Once you set them, they will be available for all subsequent pipelines. -1. Navigating to your group's **Settings > CI/CD** page. -1. Inputing variable keys and values in the **Environment variables** section. Any variables of - [subgroups](../../user/group/subgroups/index.md) will be inherited recursively. +## Priority of environment variables +{: #priority-of-variables} + +Variables of different types can take precedence over other +variables, depending on where they are defined. + +The order of precedence for variables is (from highest to lowest): + +1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables). +1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-variables). +1. Group-level [variables](#group-level-variables) or [protected variables](#protected-variables). +1. YAML-defined [job-level variables](../yaml/README.md#variables). +1. YAML-defined [global variables](../yaml/README.md#variables). +1. [Deployment variables](#deployment-variables). +1. [Predefined environment variables](predefined_variables.md). + +For example, if you define: + +- `API_TOKEN=secure` as a project variable. +- `API_TOKEN=yaml` in your `.gitlab-ci.yml`. -Once you set them, they will be available for all subsequent pipelines. You can also -[protect your variables](#protected-variables). +`API_TOKEN` will take the value `secure` as the project +variables take precedence over those defined in `.gitlab-ci.yml`. -### Protected variables +## Unsupported variables + +There are cases where some variables cannot be used in the context of a +`.gitlab-ci.yml` definition (for example under `script`). Read more +about which variables are [not supported](where_variables_can_be_used.md). + +## Advanced use + +### Protected environment variables +{: #protected-variables} > Introduced in GitLab 9.3. -Variables could be protected. Whenever a variable is +Variables can be protected. Whenever a variable is protected, it would only be securely passed to pipelines running on the [protected branches] or [protected tags]. The other pipelines would not get any protected variables. @@ -128,13 +344,8 @@ Protected variables can be added by going to your project's Once you set them, they will be available for all subsequent pipelines. -### Manually-specified variables - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/44059) in GitLab 10.8. - -Variables can be specified for a single pipeline run when a [manual pipeline](../pipelines.md#manually-executing-pipelines) is created. - -## Deployment variables +### Deployment environment variables +{: #deployment-variables} > Introduced in GitLab 8.15. @@ -147,7 +358,7 @@ the project services that you are using to learn which variables they define. An example project service that defines deployment variables is the [Kubernetes integration](../../user/project/clusters/index.md#deployment-variables). -## Auto DevOps application variables +### Auto DevOps environment variables > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/49056) in GitLab 11.7. @@ -164,6 +375,113 @@ CAUTION: **Caution:** Variables with multiline values are not currently supported due to limitations with the current Auto DevOps scripting environment. +### Environment variables triggered manually + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/44059) in GitLab 10.8. + +[Manually triggered pipelines](../pipelines.md#manually-executing-pipelines) allow you to override the value of a current variable. + +For instance, suppose you added a +[custom variable `$TEST`](#creating-a-custom-variable) +as exemplified above and you want to override it in a manual pipeline. +Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**. +Choose the branch you want to run the pipeline for, then add a new variable +pair through the UI: + +![Override variable value](img/override_variable_manual_pipeline.png) + +The Runner will override the value previously set and use the custom +value you set for this specific pipeline: + +![Manually overridden variable output](img/override_value_via_manual_pipeline_output.png) + +## Environment variables expressions +{: #variables-expressions} + +> Introduced in GitLab 10.7. + +It is possible to use variables expressions with only / except policies in +`.gitlab-ci.yml`. By using this approach you can limit what jobs are going to +be created within a pipeline after pushing a code to GitLab. + +This is particularly useful in combination with variables and triggered +pipeline variables. + +```yaml +deploy: + script: cap staging deploy + environment: staging + only: + variables: + - $RELEASE == "staging" + - $STAGING +``` + +Each expression provided is going to be evaluated before creating a pipeline. + +If any of the conditions in `variables` evaluates to truth when using `only`, +a new job is going to be created. If any of the expressions evaluates to truth +when `except` is being used, a job is not going to be created. + +This follows usual rules for [`only` / `except` policies](../yaml/README.md#onlyexcept-advanced). + +### Supported syntax + +Below you can find supported syntax reference: + +1. Equality matching using a string + + > Example: `$VARIABLE == "some value"` + + You can use equality operator `==` to compare a variable content to a + string. We support both, double quotes and single quotes to define a string + value, so both `$VARIABLE == "some value"` and `$VARIABLE == 'some value'` + are supported. `"some value" == $VARIABLE` is correct too. + +1. Checking for an undefined value + + > Example: `$VARIABLE == null` + + It sometimes happens that you want to check whether a variable is defined + or not. To do that, you can compare a variable to `null` keyword, like + `$VARIABLE == null`. This expression is going to evaluate to truth if + variable is not defined. + +1. Checking for an empty variable + + > Example: `$VARIABLE == ""` + + If you want to check whether a variable is defined, but is empty, you can + simply compare it against an empty string, like `$VAR == ''`. + +1. Comparing two variables + + > Example: `$VARIABLE_1 == $VARIABLE_2` + + It is possible to compare two variables. This is going to compare values + of these variables. + +1. Variable presence check + + > Example: `$STAGING` + + If you only want to create a job when there is some variable present, + which means that it is defined and non-empty, you can simply use + variable name as an expression, like `$STAGING`. If `$STAGING` variable + is defined, and is non empty, expression will evaluate to truth. + `$STAGING` value needs to a string, with length higher than zero. + Variable that contains only whitespace characters is not an empty variable. + +1. Pattern matching _(added in 11.0)_ + + > Example: `$VARIABLE =~ /^content.*/` + + It is possible perform pattern matching against a variable and regular + expression. Expression like this evaluates to truth if matches are found. + + Pattern matching is case-sensitive by default. Use `i` flag modifier, like + `/pattern/i` to make a pattern case-insensitive. + ## Debug tracing > Introduced in GitLab Runner 1.7. @@ -228,6 +546,8 @@ $'\''git'\'' "checkout" "-f" "-q" "dd648b2e48ce6518303b0bb580b2ee32fadaf045" Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb... ++ export CI=true ++ CI=true +++ export CI_API_V4_API_URL=https://example.com:3000/api/v4 +++ CI_API_V4_API_URL=https://example.com:3000/api/v4 ++ export CI_DEBUG_TRACE=false ++ CI_DEBUG_TRACE=false ++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045 @@ -266,6 +586,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach ++ GITLAB_CI=true ++ export CI=true ++ CI=true +++ export CI_API_V4_API_URL=https://example.com:3000/api/v4 +++ CI_API_V4_API_URL=https://example.com:3000/api/v4 ++ export GITLAB_CI=true ++ GITLAB_CI=true ++ export CI_JOB_ID=7046507 @@ -329,184 +651,6 @@ MIIFQzCCBCugAwIBAgIRAL/ElDjuf15xwja1ZnCocWAwDQYJKoZIhvcNAQELBQAw' ... ``` -## Using the CI variables in your job scripts - -All variables are set as environment variables in the build environment, and -they are accessible with normal methods that are used to access such variables. -In most cases `bash` or `sh` is used to execute the job script. - -To access environment variables, use the syntax for your Runner's [shell][shellexecutors]. - -| Shell | Usage | -|----------------------|-----------------| -| bash/sh | `$variable` | -| windows batch | `%variable%` | -| PowerShell | `$env:variable` | - -To access environment variables in bash, prefix the variable name with (`$`): - -```yaml -job_name: - script: - - echo $CI_JOB_ID -``` - -To access environment variables in **Windows Batch**, surround the variable -with (`%`): - -```yaml -job_name: - script: - - echo %CI_JOB_ID% -``` - -To access environment variables in a **Windows PowerShell** environment, prefix -the variable name with (`$env:`): - -```yaml -job_name: - script: - - echo $env:CI_JOB_ID -``` - -You can also list all environment variables with the `export` command, -but be aware that this will also expose the values of all the variables -you set, in the job log: - -```yaml -job_name: - script: - - export -``` - -Example values: - -```bash -export CI_JOB_ID="50" -export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a" -export CI_COMMIT_SHORT_SHA="1ecfd275" -export CI_COMMIT_REF_NAME="master" -export CI_REPOSITORY_URL="https://gitlab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" -export CI_COMMIT_TAG="1.0.0" -export CI_JOB_NAME="spec:other" -export CI_JOB_STAGE="test" -export CI_JOB_MANUAL="true" -export CI_JOB_TRIGGERED="true" -export CI_JOB_TOKEN="abcde-1234ABCD5678ef" -export CI_PIPELINE_ID="1000" -export CI_PIPELINE_IID="10" -export CI_PAGES_DOMAIN="gitlab.io" -export CI_PAGES_URL="https://gitlab-org.gitlab.io/gitlab-ce" -export CI_PROJECT_ID="34" -export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" -export CI_PROJECT_NAME="gitlab-ce" -export CI_PROJECT_NAMESPACE="gitlab-org" -export CI_PROJECT_PATH="gitlab-org/gitlab-ce" -export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce" -export CI_REGISTRY="registry.example.com" -export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce" -export CI_RUNNER_ID="10" -export CI_RUNNER_DESCRIPTION="my runner" -export CI_RUNNER_TAGS="docker, linux" -export CI_SERVER="yes" -export CI_SERVER_NAME="GitLab" -export CI_SERVER_REVISION="70606bf" -export CI_SERVER_VERSION="8.9.0" -export CI_SERVER_VERSION_MAJOR="8" -export CI_SERVER_VERSION_MINOR="9" -export CI_SERVER_VERSION_PATCH="0" -export GITLAB_USER_ID="42" -export GITLAB_USER_EMAIL="user@example.com" -export CI_REGISTRY_USER="gitlab-ci-token" -export CI_REGISTRY_PASSWORD="longalfanumstring" -``` - -## Variables expressions - -> Introduced in GitLab 10.7. - -It is possible to use variables expressions with only / except policies in -`.gitlab-ci.yml`. By using this approach you can limit what jobs are going to -be created within a pipeline after pushing a code to GitLab. - -This is particularly useful in combination with variables and triggered -pipeline variables. - -```yaml -deploy: - script: cap staging deploy - environment: staging - only: - variables: - - $RELEASE == "staging" - - $STAGING -``` - -Each expression provided is going to be evaluated before creating a pipeline. - -If any of the conditions in `variables` evaluates to truth when using `only`, -a new job is going to be created. If any of the expressions evaluates to truth -when `except` is being used, a job is not going to be created. - -This follows usual rules for [`only` / `except` policies](../yaml/README.md#onlyexcept-advanced). - -### Supported syntax - -Below you can find supported syntax reference: - -1. Equality matching using a string - - > Example: `$VARIABLE == "some value"` - - You can use equality operator `==` to compare a variable content to a - string. We support both, double quotes and single quotes to define a string - value, so both `$VARIABLE == "some value"` and `$VARIABLE == 'some value'` - are supported. `"some value" == $VARIABLE` is correct too. - -1. Checking for an undefined value - - > Example: `$VARIABLE == null` - - It sometimes happens that you want to check whether a variable is defined - or not. To do that, you can compare a variable to `null` keyword, like - `$VARIABLE == null`. This expression is going to evaluate to truth if - variable is not defined. - -1. Checking for an empty variable - - > Example: `$VARIABLE == ""` - - If you want to check whether a variable is defined, but is empty, you can - simply compare it against an empty string, like `$VAR == ''`. - -1. Comparing two variables - - > Example: `$VARIABLE_1 == $VARIABLE_2` - - It is possible to compare two variables. This is going to compare values - of these variables. - -1. Variable presence check - - > Example: `$STAGING` - - If you only want to create a job when there is some variable present, - which means that it is defined and non-empty, you can simply use - variable name as an expression, like `$STAGING`. If `$STAGING` variable - is defined, and is non empty, expression will evaluate to truth. - `$STAGING` value needs to a string, with length higher than zero. - Variable that contains only whitespace characters is not an empty variable. - -1. Pattern matching _(added in 11.0)_ - - > Example: `$VARIABLE =~ /^content.*/` - - It is possible perform pattern matching against a variable and regular - expression. Expression like this evaluates to truth if matches are found. - - Pattern matching is case-sensitive by default. Use `i` flag modifier, like - `/pattern/i` to make a pattern case-insensitive. - [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables" [eep]: https://about.gitlab.com/pricing/ "Available only in GitLab Premium" [envs]: ../environments.md diff --git a/doc/ci/variables/img/ci_job_stage_output_example.png b/doc/ci/variables/img/ci_job_stage_output_example.png Binary files differnew file mode 100755 index 00000000000..056238d5693 --- /dev/null +++ b/doc/ci/variables/img/ci_job_stage_output_example.png diff --git a/doc/ci/variables/img/custom_variable_output.png b/doc/ci/variables/img/custom_variable_output.png Binary files differnew file mode 100755 index 00000000000..50f3bceff9a --- /dev/null +++ b/doc/ci/variables/img/custom_variable_output.png diff --git a/doc/ci/variables/img/new_custom_variable_example.png b/doc/ci/variables/img/new_custom_variable_example.png Binary files differnew file mode 100755 index 00000000000..d169c5f1806 --- /dev/null +++ b/doc/ci/variables/img/new_custom_variable_example.png diff --git a/doc/ci/variables/img/override_value_via_manual_pipeline_output.png b/doc/ci/variables/img/override_value_via_manual_pipeline_output.png Binary files differnew file mode 100755 index 00000000000..02369d57fb8 --- /dev/null +++ b/doc/ci/variables/img/override_value_via_manual_pipeline_output.png diff --git a/doc/ci/variables/img/override_variable_manual_pipeline.png b/doc/ci/variables/img/override_variable_manual_pipeline.png Binary files differnew file mode 100755 index 00000000000..3bcd354e096 --- /dev/null +++ b/doc/ci/variables/img/override_variable_manual_pipeline.png diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5690729c370..e75f7050a09 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -391,6 +391,11 @@ job: The above example will run `job` for all branches on `gitlab-org/gitlab-ce`, except `master` and those with names prefixed with `release/`. +NOTE: **Note:** +Because `@` is used to denote the beginning of a ref's repository path, +matching a ref name containing the `@` character in a regular expression +requires the use of the hex character code match `\x40`. + If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by default. If it doesn't have an `except` rule, it is empty. @@ -496,7 +501,7 @@ Learn more about [variables expressions](../variables/README.md#variables-expres #### `only:changes`/`except:changes` -Using the `changes` keyword with `only` or `except`, makes it possible to define if +Using the `changes` keyword with `only` or `except` makes it possible to define if a job should be created based on files modified by a git push event. For example: @@ -513,14 +518,38 @@ docker build: ``` In the scenario above, when pushing multiple commits to GitLab to an existing -branch, GitLab creates and triggers `docker build` job, provided that one of the -commits contains changes to either: +branch, GitLab creates and triggers the `docker build` job, provided that one of the +commits contains changes to any of the following: - The `Dockerfile` file. - Any of the files inside `docker/scripts/` directory. - Any of the files and subdirectories inside the `dockerfiles` directory. - Any of the files with `rb`, `py`, `sh` extensions inside the `more_scripts` directory. +You can also use glob patterns to match multiple files in either the root directory of the repo, or in _any_ directory within the repo. For example: + +```yaml +test: + script: npm run test + only: + changes: + - "*.json" + - "**/*.sql" +``` + +NOTE: **Note:** +In the example above, the expressions are wrapped double quotes because they are glob patterns. GitLab will fail to parse `.gitlab-ci.yml` files with unwrapped glob patterns. + +The following example will skip the CI job if a change is detected in any file in the root directory of the repo with a `.md` extension: + +```yaml +build: + script: npm run build + except: + changes: + - "*.md" +``` + CAUTION: **Warning:** There are some caveats when using this feature with new branches and tags. See the section below. diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index c2e05b2d065..9452593c510 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -943,7 +943,7 @@ import mixin from 'ee_else_ce/path/mixin'; ```html - <ul v-if="renderIfEE"> + <ul v-if="ifEE"> <li>One wrapped</li> <li>element</li> <li>that is rendered</li> @@ -962,7 +962,7 @@ For regular JS files, the approach is similar. ```javascript import { ifEE } from '~/lib/utils/common_utils' -if (renderIfEE) { +if (ifEE) { $('.js-import-git-toggle-button').on('click', () => { const $projectMirror = $('#project_mirror'); @@ -976,7 +976,7 @@ if (renderIfEE) { To separate EE-specific styles in SCSS files, if a component you're adding styles for is limited to only EE, it is better to have a separate SCSS file in appropriate directory within `app/assets/stylesheets`. -See [backporting changes](#backporting-changes-from-EE-to-CE) for instructions on how to merge changes safely. +See [backporting changes](#backporting-changes-from-ee-to-ce) for instructions on how to merge changes safely. In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill, e.g. a text style of some component is different for EE. In such cases, diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 71c9637e72c..f58a8dcbcdc 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -26,6 +26,10 @@ It is not yet a requirement to use Jest. You can view the [epic](https://gitlab.com/groups/gitlab-org/-/epics/873) of issues we need to solve before being able to use Jest for all our needs. +### Debugging Jest tests + +Running `yarn jest-debug` will run Jest in debug mode, allowing you to debug/inspect as described in the [Jest docs](https://jestjs.io/docs/en/troubleshooting#tests-are-failing-and-you-don-t-know-why). + ### Timeout error The default timeout for Jest is set in diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index fd99b06b863..7693109b3c4 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -699,6 +699,21 @@ renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of the `production` job to turn it into a manual action instead of deploying automatically. +### Using components of Auto-DevOps + +If you only require a subset of the features offered by Auto-DevOps, you can include +individual Auto-DevOps jobs into your own `.gitlab-ci.yml`. + +For example, to make use of [Auto Build](#auto-build), you can add the following to +your `.gitlab-ci.yml`: + +```yaml +include: + - template: Jobs/Build.gitlab-ci.yml +``` + +Consult the [Auto DevOps template] for information on available jobs. + ### PostgreSQL database support In order to support applications that require a database, diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index 350072186ee..b7f7d71689d 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,31 +1,58 @@ --- -last_updated: 2018-02-07 +last_updated: 2019-03-27 --- # Migrating from MySQL to PostgreSQL -> **Note:** This guide assumes you have a working GitLab instance with -> MySQL and want to migrate to bundled PostgreSQL database. +This guide documents how to take a working GitLab instance that uses MySQL and +migrate it to a PostgreSQL database. -## Omnibus installation +## Requirements -### Prerequisites +[pgloader](http://pgloader.io) 3.4.1+ is required. -First, we'll need to enable the bundled PostgreSQL database with up-to-date -schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data -from the old MySQL database to the new PostgreSQL one. +You can install it directly from your distribution, for example in +Debian/Ubuntu: -Here's what you'll need to have installed: +1. Search for the version: -- pgloader 3.4.1+ -- Omnibus GitLab -- MySQL + ```bash + apt-cache madison pgloader + ``` -### Enable bundled PostgreSQL database +1. If the version is 3.4.1+, install it with: + + ```bash + sudo apt-get install pgloader + ``` + + If your distribution's version is too old, use PostgreSQL's repository: + + ```bash + # Add repository + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + + # Add key + sudo apt-get install wget ca-certificates + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + + # Install package + sudo apt-get update + sudo apt-get install pgloader + ``` + +For other distributions, follow the instructions in PostrgreSQL's +[download page](https://www.postgresql.org/download/) to add their repository +and then install `pgloader`. + +## Omnibus GitLab installations + +For [Omnibus GitLab packages](https://about.gitlab.com/install/), you'll first +need to enable the bundled PostgreSQL: 1. Stop GitLab: - ``` bash + ```bash sudo gitlab-ctl stop ``` @@ -40,39 +67,34 @@ Here's what you'll need to have installed: and alike. You could just comment all of them out so that we'll just use the defaults. -1. [Reconfigure GitLab] for the changes to take effect: - - ``` bash - sudo gitlab-ctl reconfigure - ``` - +1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) + for the changes to take effect. 1. Start Unicorn and PostgreSQL so that we can prepare the schema: - ``` bash + ```bash sudo gitlab-ctl start unicorn sudo gitlab-ctl start postgresql ``` 1. Run the following commands to prepare the schema: - ``` bash + ```bash sudo gitlab-rake db:create db:migrate ``` 1. Stop Unicorn to prevent other database access from interfering with the loading of data: - ``` bash + ```bash sudo gitlab-ctl stop unicorn ``` After these steps, you'll have a fresh PostgreSQL database with up-to-date schema. -### Migrate data from MySQL to PostgreSQL - -Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: +Next, we'll use `pgloader` to migrate the data from the old MySQL database to the +new PostgreSQL one: 1. Save the following snippet in a `commands.load` file, and edit with your - database `username`, `password` and `host`: + MySQL database `username`, `password` and `host`: ``` LOAD DATABASE @@ -90,7 +112,7 @@ Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: 1. Start the migration: - ``` bash + ```bash sudo -u gitlab-psql pgloader commands.load ``` @@ -117,170 +139,140 @@ Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: Total import time 1894 1894 0 12.497s ``` - If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See - the [troubleshooting guide](#Troubleshooting) for more details. + If there is no output for more than 30 minutes, it's possible `pgloader` encountered an error. See + the [troubleshooting guide](#troubleshooting) for more details. 1. Start GitLab: - ``` bash + ```bash sudo gitlab-ctl start ``` -Now, you can verify that everything worked by visiting GitLab. - -### Troubleshooting - -#### Permissions - -Note that the PostgreSQL user that you use for the above MUST have **superuser** privileges. Otherwise, you may see -a similar message to the following: - -``` -debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread - #<THREAD "lparallel" RUNNING {10078A3513}>: - Database error 42501: permission denied: "RI_ConstraintTrigger_a_20937" is a system trigger - QUERY: ALTER TABLE ci_builds DISABLE TRIGGER ALL; - 2017-08-23T00:36:56.782000Z ERROR Database error 42501: permission denied: "RI_ConstraintTrigger_c_20864" is a system trigger - QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL; -``` - -#### Experiencing 500 errors after the migration - -If you experience 500 errors after the migration, try to clear the cache: - -``` bash -sudo gitlab-rake cache:clear -``` - -[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure - -## Source installation +You can now verify that everything works as expected by visiting GitLab. -### Prerequisites +## Source installations -#### Install PostgreSQL and create database +For installations from source that use MySQL, you'll first need to +[install PostgreSQL and create a database](../install/installation.md#6-database). -See [installation guide](../install/installation.md#6-database). - -#### Install [pgloader](http://pgloader.io) 3.4.1+ - -Install directly from your distro: -``` bash -sudo apt-get install pgloader -``` - -If this version is too old, use PostgreSQL's repository: -``` bash -# add repository -sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' - -# add key -sudo apt-get install wget ca-certificates -wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - - -# install package -sudo apt-get update -sudo apt-get install pgloader -``` - -### Enable bundled PostgreSQL database +After the database is created, go on with the following steps: 1. Stop GitLab: - ``` bash - sudo service gitlab stop - ``` + ```bash + sudo service gitlab stop + ``` 1. Switch database from MySQL to PostgreSQL - ``` bash - cd /home/git/gitlab - sudo -u git mv config/database.yml config/database.yml.bak - sudo -u git cp config/database.yml.postgresql config/database.yml - sudo -u git -H chmod o-rwx config/database.yml - ``` + ```bash + cd /home/git/gitlab + sudo -u git mv config/database.yml config/database.yml.bak + sudo -u git cp config/database.yml.postgresql config/database.yml + sudo -u git -H chmod o-rwx config/database.yml + ``` + 1. Install Gems related to Postgresql - ``` bash - sudo -u git -H rm .bundle/config - sudo -u git -H bundle install --deployment --without development test mysql aws kerberos - ``` + ```bash + sudo -u git -H rm .bundle/config + sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + ``` 1. Run the following commands to prepare the schema: - ``` bash - sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production - ``` + ```bash + sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production + ``` After these steps, you'll have a fresh PostgreSQL database with up-to-date schema. -### Migrate data from MySQL to PostgreSQL - -Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: +Next, we'll use `pgloader` to migrate the data from the old MySQL database to the +new PostgreSQL one: 1. Save the following snippet in a `commands.load` file, and edit with your MySQL `username`, `password` and `host`: - ``` - LOAD DATABASE - FROM mysql://username:password@host/gitlabhq_production - INTO postgresql://postgres@unix://var/run/postgresql:/gitlabhq_production + ``` + LOAD DATABASE + FROM mysql://username:password@host/gitlabhq_production + INTO postgresql://postgres@unix://var/run/postgresql:/gitlabhq_production - WITH include no drop, truncate, disable triggers, create no tables, - create no indexes, preserve index names, no foreign keys, - data only + WITH include no drop, truncate, disable triggers, create no tables, + create no indexes, preserve index names, no foreign keys, + data only - ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public' + ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public' - ; - ``` + ; + ``` 1. Start the migration: - ``` bash - sudo -u postgres pgloader commands.load - ``` + ```bash + sudo -u postgres pgloader commands.load + ``` 1. Once the migration finishes, you should see a summary table that looks like the following: - ``` - table name read imported errors total time - ----------------------------------------------- --------- --------- --------- -------------- - fetch meta data 119 119 0 0.388s - Truncate 119 119 0 1.134s - ----------------------------------------------- --------- --------- --------- -------------- - public.abuse_reports 0 0 0 0.490s - public.appearances 0 0 0 0.488s - . - . - . - public.web_hook_logs 0 0 0 1.080s - ----------------------------------------------- --------- --------- --------- -------------- - COPY Threads Completion 4 4 0 2.008s - Reset Sequences 113 113 0 0.304s - Install Comments 0 0 0 0.000s - ----------------------------------------------- --------- --------- --------- -------------- - Total import time 1894 1894 0 12.497s - ``` - - If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See - the [troubleshooting guide](#Troubleshooting) for more details. + ``` + table name read imported errors total time + ----------------------------------------------- --------- --------- --------- -------------- + fetch meta data 119 119 0 0.388s + Truncate 119 119 0 1.134s + ----------------------------------------------- --------- --------- --------- -------------- + public.abuse_reports 0 0 0 0.490s + public.appearances 0 0 0 0.488s + . + . + . + public.web_hook_logs 0 0 0 1.080s + ----------------------------------------------- --------- --------- --------- -------------- + COPY Threads Completion 4 4 0 2.008s + Reset Sequences 113 113 0 0.304s + Install Comments 0 0 0 0.000s + ----------------------------------------------- --------- --------- --------- -------------- + Total import time 1894 1894 0 12.497s + ``` + + If there is no output for more than 30 minutes, it's possible `pgloader` encountered an error. See + the [troubleshooting guide](#troubleshooting) for more details. 1. Start GitLab: - ``` bash - sudo service gitlab start - ``` + ```bash + sudo service gitlab start + ``` + +You can now verify that everything works as expected by visiting GitLab. + +## Troubleshooting + +Sometimes, you might encounter some errors during or after the migration. -Now, you can verify that everything worked by visiting GitLab. +### Database error permission denied -### Troubleshooting +The PostgreSQL user that you use for the migration MUST have **superuser** privileges. +Otherwise, you may see a similar message to the following: -#### Experiencing 500 errors after the migration +``` +debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread + #<THREAD "lparallel" RUNNING {10078A3513}>: + Database error 42501: permission denied: "RI_ConstraintTrigger_a_20937" is a system trigger + QUERY: ALTER TABLE ci_builds DISABLE TRIGGER ALL; + 2017-08-23T00:36:56.782000Z ERROR Database error 42501: permission denied: "RI_ConstraintTrigger_c_20864" is a system trigger + QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL; +``` + +### Experiencing 500 errors after the migration If you experience 500 errors after the migration, try to clear the cache: -``` bash +```bash +# Omnibus GitLab +sudo gitlab-rake cache:clear + +# Installations from source sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production ``` diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 8b3ae19b544..df413a11af0 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -161,8 +161,8 @@ a new set of recovery codes with SSH. 1. Run `ssh git@gitlab.example.com 2fa_recovery_codes`. 1. You are prompted to confirm that you want to generate new codes. Continuing this process invalidates previously saved codes. - ``` - bash + + ```sh $ ssh git@gitlab.example.com 2fa_recovery_codes Are you sure you want to generate new two-factor recovery codes? Any existing recovery codes you saved will be invalidated. (yes/no) @@ -207,17 +207,17 @@ Sign in and re-enable two-factor authentication as soon as possible. - You need to take special care to that 2FA keeps working after [restoring a GitLab backup](../../../raketasks/backup_restore.md). - To ensure 2FA authorizes correctly with TOTP server, you may want to ensure - your GitLab server's time is synchronized via a service like NTP. Otherwise, + your GitLab server's time is synchronized via a service like NTP. Otherwise, you may have cases where authorization always fails because of time differences. - The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at the time of registration, and cannot be used for other hostnames/FQDNs. - For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: + For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: - - The user logs in via `first.host.xyz` and registers their U2F key. - - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication succeeds. - - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because + - The user logs in via `first.host.xyz` and registers their U2F key. + - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication succeeds. + - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because the U2F key has only been registered on `first.host.xyz`. [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 3a9a3b4a423..c94a3f4d3b5 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -314,12 +314,6 @@ install it manually. ## Installing applications -NOTE: **Note:** -Before starting the installation of applications, make sure that time is synchronized -between your GitLab server and your Kubernetes cluster. Otherwise, installation could fail -and you may get errors like `Error: remote error: tls: bad certificate` -in the `stdout` of pods created by GitLab in your Kubernetes cluster. - GitLab provides a one-click install for various applications which can be added directly to your configured cluster. Those applications are needed for [Review Apps](../../../ci/review_apps/index.md) and @@ -378,6 +372,29 @@ Upgrades will reset values back to the values built into the `runner` chart plus the values set by [`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml) +### Troubleshooting applications + +Applications can fail with the following error: + +```text +Error: remote error: tls: bad certificate +``` + +To avoid installation errors: + +- Before starting the installation of applications, make sure that time is synchronized + between your GitLab server and your Kubernetes cluster. +- Ensure certificates are not out of sync. When installing applications, GitLab expects a new cluster with no previous installation of Tiller. + + You can confirm that the certificates match via `kubectl`: + + ```sh + kubectl get configmaps/values-content-configuration-ingress -n gitlab-managed-apps -o \ + "jsonpath={.data['cert\.pem']}" | base64 -d > a.pem + kubectl get secrets/tiller-secret -n gitlab-managed-apps -o "jsonpath={.data['ca\.crt']}" | base64 -d > b.pem + diff a.pem b.pem + ``` + ## Getting the external endpoint NOTE: **Note:** diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index e6804666e22..96bf455116c 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -214,7 +214,7 @@ The sample function can now be triggered from any HTTP client using a simple `PO --header "Content-Type: application/json" \ --request POST \ --data '{"GitLab":"FaaS"}' \ - <http://functions-echo.functions-1.functions.example.com/> + http://functions-echo.functions-1.functions.example.com ``` 2. Using a web-based tool (ie. postman, restlet, etc) diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 392e72dcc5c..88f4de891a1 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -31,7 +31,7 @@ discussions, and descriptions: | `/reassign @user1 @user2` | Change assignee | ✓ | ✓ | | `/milestone %milestone` | Set milestone | ✓ | ✓ | | `/remove_milestone` | Remove milestone | ✓ | ✓ | -| `/label ~label1 ~label2` | Add label(s) | ✓ | ✓ | +| `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ | | `/unlabel ~label1 ~label2` | Remove all or specific label(s)| ✓ | ✓ | | `/relabel ~label1 ~label2` | Replace label | ✓ | ✓ | | <code>/copy_metadata #issue | !merge_request</code> | Copy labels and milestone from other issue or merge request | ✓ | ✓ | diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4533305bfd3..2dd3120d3fc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -690,6 +690,10 @@ module API # Deprecated expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? } + expose :reference do |merge_request, options| + merge_request.to_reference(options[:project]) + end + expose :web_url do |merge_request| Gitlab::UrlBuilder.build(merge_request) end @@ -726,6 +730,8 @@ module API merge_request.metrics&.pipeline end + expose :head_pipeline, using: 'API::Entities::Pipeline' + expose :diff_refs, using: Entities::DiffRefs # Allow the status of a rebase to be determined @@ -1267,6 +1273,9 @@ module API expose :created_at, :updated_at, :started_at, :finished_at, :committed_at expose :duration expose :coverage + expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| + pipeline.detailed_status(options[:current_user]) + end end class PipelineSchedule < Grape::Entity @@ -1389,8 +1398,13 @@ module API expose :name, :script, :timeout, :when, :allow_failure end + class Port < Grape::Entity + expose :number, :protocol, :name + end + class Image < Grape::Entity expose :name, :entrypoint + expose :ports, using: JobRequest::Port end class Service < Image diff --git a/lib/api/issues.rb b/lib/api/issues.rb index fae20e45bf9..3dd90502050 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -310,7 +310,7 @@ module API .flatten present paginate(::Kaminari.paginate_array(merge_requests)), - with: Entities::MergeRequestBasic, + with: Entities::MergeRequest, current_user: current_user, project: user_project end diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index 4dd932f61d4..1d7bfba75cd 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -4,7 +4,7 @@ module Gitlab module Ci module Build class Image - attr_reader :alias, :command, :entrypoint, :name + attr_reader :alias, :command, :entrypoint, :name, :ports class << self def from_image(job) @@ -26,17 +26,25 @@ module Gitlab def initialize(image) if image.is_a?(String) @name = image + @ports = [] elsif image.is_a?(Hash) @alias = image[:alias] @command = image[:command] @entrypoint = image[:entrypoint] @name = image[:name] + @ports = build_ports(image).select(&:valid?) end end def valid? @name.present? end + + private + + def build_ports(image) + image[:ports].to_a.map { |port| ::Gitlab::Ci::Build::Port.new(port) } + end end end end diff --git a/lib/gitlab/ci/build/port.rb b/lib/gitlab/ci/build/port.rb new file mode 100644 index 00000000000..6c4656ffea2 --- /dev/null +++ b/lib/gitlab/ci/build/port.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Port + DEFAULT_PORT_NAME = 'default_port'.freeze + DEFAULT_PORT_PROTOCOL = 'http'.freeze + + attr_reader :number, :protocol, :name + + def initialize(port) + @name = DEFAULT_PORT_NAME + @protocol = DEFAULT_PORT_PROTOCOL + + case port + when Integer + @number = port + when Hash + @number = port[:number] + @protocol = port.fetch(:protocol, @protocol) + @name = port.fetch(:name, @name) + end + end + + def valid? + @number.present? + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index a13a0625e90..0beeb44c272 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -9,24 +9,24 @@ module Gitlab # class Image < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Configurable - ALLOWED_KEYS = %i[name entrypoint].freeze + ALLOWED_KEYS = %i[name entrypoint ports].freeze validations do validates :config, hash_or_string: true validates :config, allowed_keys: ALLOWED_KEYS + validates :config, disallowed_keys: %i[ports], unless: :with_image_ports? validates :name, type: String, presence: true validates :entrypoint, array_of_strings: true, allow_nil: true end - def hash? - @config.is_a?(Hash) - end + entry :ports, Entry::Ports, + description: 'Ports used expose the image' - def string? - @config.is_a?(String) - end + attributes :ports def name value[:name] @@ -42,6 +42,14 @@ module Gitlab {} end + + def with_image_ports? + opt(:with_image_ports) + end + + def skip_config_hash_validation? + true + end end end end diff --git a/lib/gitlab/ci/config/entry/port.rb b/lib/gitlab/ci/config/entry/port.rb new file mode 100644 index 00000000000..c239b1225c5 --- /dev/null +++ b/lib/gitlab/ci/config/entry/port.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a configuration of an Image Port. + # + class Port < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_KEYS = %i[number protocol name].freeze + + validations do + validates :config, hash_or_integer: true + validates :config, allowed_keys: ALLOWED_KEYS + + validates :number, type: Integer, presence: true + validates :protocol, type: String, inclusion: { in: %w[http https], message: 'should be http or https' }, allow_blank: true + validates :name, type: String, presence: false, allow_nil: true + end + + def number + value[:number] + end + + def protocol + value[:protocol] + end + + def name + value[:name] + end + + def value + return { number: @config } if integer? + return @config if hash? + + {} + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/ports.rb b/lib/gitlab/ci/config/entry/ports.rb new file mode 100644 index 00000000000..01ffcc7dd87 --- /dev/null +++ b/lib/gitlab/ci/config/entry/ports.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a configuration of the ports of a Docker service. + # + class Ports < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, type: Array + validates :config, port_name_present_and_unique: true + validates :config, port_unique: true + end + + def compose!(deps = nil) + super do + @entries = [] + @config.each do |config| + @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port) + .value(config || {}) + .with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord + .create! + end + + @entries.each do |entry| + entry.compose!(deps) + end + end + end + + def value + @entries.map(&:value) + end + + def descendants + @entries + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index 6df67083310..084fa4047a4 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -10,16 +10,18 @@ module Gitlab class Service < Image include ::Gitlab::Config::Entry::Validatable - ALLOWED_KEYS = %i[name entrypoint command alias].freeze + ALLOWED_KEYS = %i[name entrypoint command alias ports].freeze validations do validates :config, hash_or_string: true validates :config, allowed_keys: ALLOWED_KEYS + validates :config, disallowed_keys: %i[ports], unless: :with_image_ports? validates :name, type: String, presence: true validates :entrypoint, array_of_strings: true, allow_nil: true validates :command, array_of_strings: true, allow_nil: true validates :alias, type: String, allow_nil: true + validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? } end def alias diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index 71475f69218..83baa83711f 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -12,6 +12,7 @@ module Gitlab validations do validates :config, type: Array + validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) } end def compose!(deps = nil) @@ -20,6 +21,7 @@ module Gitlab @config.each do |config| @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service) .value(config || {}) + .with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord .create! end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index d4c70d4b9da..f786022beb0 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -72,879 +72,14 @@ stages: - performance - cleanup -build: - stage: build - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable" - services: - - docker:stable-dind - script: - - /build/build.sh - only: - - branches - - tags - -test: - services: - - postgres:latest - variables: - POSTGRES_DB: test - stage: test - image: gliderlabs/herokuish:latest - script: - - setup_test_db - - cp -R . /tmp/app - - /bin/herokuish buildpack test - only: - - branches - - tags - except: - variables: - - $TEST_DISABLED - -code_quality: - stage: test - image: docker:stable - allow_failure: true - services: - - docker:stable-dind - script: - - setup_docker - - code_quality - artifacts: - paths: [gl-code-quality-report.json] - only: - - branches - - tags - except: - variables: - - $CODE_QUALITY_DISABLED - -license_management: - stage: test - image: - name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" - entrypoint: [""] - allow_failure: true - script: - - license_management - artifacts: - paths: [gl-license-management-report.json] - only: - refs: - - branches - - tags - variables: - - $GITLAB_FEATURES =~ /\blicense_management\b/ - except: - variables: - - $LICENSE_MANAGEMENT_DISABLED - -performance: - stage: performance - image: docker:stable - allow_failure: true - services: - - docker:stable-dind - script: - - setup_docker - - performance - artifacts: - paths: - - performance.json - - sitespeed-results/ - only: - refs: - - branches - - tags - kubernetes: active - except: - variables: - - $PERFORMANCE_DISABLED - -sast: - stage: test - image: docker:stable - allow_failure: true - services: - - docker:stable-dind - script: - - setup_docker - - sast - artifacts: - reports: - sast: gl-sast-report.json - only: - refs: - - branches - - tags - variables: - - $GITLAB_FEATURES =~ /\bsast\b/ - except: - variables: - - $SAST_DISABLED - -dependency_scanning: - stage: test - image: docker:stable - allow_failure: true - services: - - docker:stable-dind - script: - - setup_docker - - dependency_scanning - artifacts: - reports: - dependency_scanning: gl-dependency-scanning-report.json - only: - refs: - - branches - - tags - variables: - - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ - except: - variables: - - $DEPENDENCY_SCANNING_DISABLED - -container_scanning: - stage: test - image: docker:stable - allow_failure: true - services: - - docker:stable-dind - script: - - setup_docker - - container_scanning - artifacts: - paths: [gl-container-scanning-report.json] - only: - refs: - - branches - - tags - variables: - - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/ - except: - variables: - - $CONTAINER_SCANNING_DISABLED - -dast: - stage: dast - allow_failure: true - image: registry.gitlab.com/gitlab-org/security-products/zaproxy - variables: - POSTGRES_DB: "false" - script: - - dast - artifacts: - paths: [gl-dast-report.json] - only: - refs: - - branches - - tags - kubernetes: active - variables: - - $GITLAB_FEATURES =~ /\bdast\b/ - except: - refs: - - master - variables: - - $DAST_DISABLED - -review: - stage: review - script: - - check_kube_domain - - install_dependencies - - download_chart - - ensure_namespace - - initialize_tiller - - create_secret - - deploy - - persist_environment_url - environment: - name: review/$CI_COMMIT_REF_NAME - url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN - on_stop: stop_review - artifacts: - paths: [environment_url.txt] - only: - refs: - - branches - - tags - kubernetes: active - except: - refs: - - master - variables: - - $REVIEW_DISABLED - -stop_review: - stage: cleanup - variables: - GIT_STRATEGY: none - script: - - install_dependencies - - initialize_tiller - - delete - environment: - name: review/$CI_COMMIT_REF_NAME - action: stop - when: manual - allow_failure: true - only: - refs: - - branches - - tags - kubernetes: active - except: - refs: - - master - variables: - - $REVIEW_DISABLED - -# Staging deploys are disabled by default since -# continuous deployment to production is enabled by default -# If you prefer to automatically deploy to staging and -# only manually promote to production, enable this job by setting -# STAGING_ENABLED. - -staging: - stage: staging - script: - - check_kube_domain - - install_dependencies - - download_chart - - ensure_namespace - - initialize_tiller - - create_secret - - deploy - environment: - name: staging - url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN - only: - refs: - - master - kubernetes: active - variables: - - $STAGING_ENABLED - -# Canaries are also disabled by default, but if you want them, -# and know what the downsides are, you can enable this by setting -# CANARY_ENABLED. - -canary: - stage: canary - script: - - check_kube_domain - - install_dependencies - - download_chart - - ensure_namespace - - initialize_tiller - - create_secret - - deploy canary - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN - when: manual - only: - refs: - - master - kubernetes: active - variables: - - $CANARY_ENABLED - -.production: &production_template - stage: production - script: - - check_kube_domain - - install_dependencies - - download_chart - - ensure_namespace - - initialize_tiller - - create_secret - - deploy - - delete canary - - delete rollout - - persist_environment_url - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN - artifacts: - paths: [environment_url.txt] - -production: - <<: *production_template - only: - refs: - - master - kubernetes: active - except: - variables: - - $STAGING_ENABLED - - $CANARY_ENABLED - - $INCREMENTAL_ROLLOUT_ENABLED - - $INCREMENTAL_ROLLOUT_MODE - -production_manual: - <<: *production_template - when: manual - allow_failure: false - only: - refs: - - master - kubernetes: active - variables: - - $STAGING_ENABLED - - $CANARY_ENABLED - except: - variables: - - $INCREMENTAL_ROLLOUT_ENABLED - - $INCREMENTAL_ROLLOUT_MODE - -# This job implements incremental rollout on for every push to `master`. - -.rollout: &rollout_template - script: - - check_kube_domain - - install_dependencies - - download_chart - - ensure_namespace - - initialize_tiller - - create_secret - - deploy rollout $ROLLOUT_PERCENTAGE - - scale stable $((100-ROLLOUT_PERCENTAGE)) - - delete canary - - persist_environment_url - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN - artifacts: - paths: [environment_url.txt] - -.manual_rollout_template: &manual_rollout_template - <<: *rollout_template - stage: production - when: manual - # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4) - only: - refs: - - master - kubernetes: active - variables: - - $INCREMENTAL_ROLLOUT_MODE == "manual" - - $INCREMENTAL_ROLLOUT_ENABLED - except: - variables: - - $INCREMENTAL_ROLLOUT_MODE == "timed" - -.timed_rollout_template: &timed_rollout_template - <<: *rollout_template - when: delayed - start_in: 5 minutes - only: - refs: - - master - kubernetes: active - variables: - - $INCREMENTAL_ROLLOUT_MODE == "timed" - -timed rollout 10%: - <<: *timed_rollout_template - stage: incremental rollout 10% - variables: - ROLLOUT_PERCENTAGE: 10 - -timed rollout 25%: - <<: *timed_rollout_template - stage: incremental rollout 25% - variables: - ROLLOUT_PERCENTAGE: 25 - -timed rollout 50%: - <<: *timed_rollout_template - stage: incremental rollout 50% - variables: - ROLLOUT_PERCENTAGE: 50 - -timed rollout 100%: - <<: *timed_rollout_template - <<: *production_template - stage: incremental rollout 100% - variables: - ROLLOUT_PERCENTAGE: 100 - -rollout 10%: - <<: *manual_rollout_template - variables: - ROLLOUT_PERCENTAGE: 10 - -rollout 25%: - <<: *manual_rollout_template - variables: - ROLLOUT_PERCENTAGE: 25 - -rollout 50%: - <<: *manual_rollout_template - variables: - ROLLOUT_PERCENTAGE: 50 - -rollout 100%: - <<: *manual_rollout_template - <<: *production_template - allow_failure: false - -# --------------------------------------------------------------------------- - -.auto_devops: &auto_devops | - # Auto DevOps variables and functions - [[ "$TRACE" ]] && set -x - auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB} - export DATABASE_URL=${DATABASE_URL-$auto_database_url} - if [[ -z "$CI_COMMIT_TAG" ]]; then - export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG - export CI_APPLICATION_TAG=$CI_COMMIT_SHA - else - export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE - export CI_APPLICATION_TAG=$CI_COMMIT_TAG - fi - export TILLER_NAMESPACE=$KUBE_NAMESPACE - # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - function registry_login() { - if [[ -n "$CI_REGISTRY_USER" ]]; then - echo "Logging to GitLab Container Registry with CI credentials..." - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - echo "" - fi - } - - function container_scanning() { - registry_login - - docker run -d --name db arminc/clair-db:latest - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6 - apk add -U wget ca-certificates - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 - mv clair-scanner_linux_amd64 clair-scanner - chmod +x clair-scanner - touch clair-whitelist.yml - retries=0 - echo "Waiting for clair daemon to start" - while( ! wget -T 10 -q -O /dev/null http://${DOCKER_SERVICE}:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done - ./clair-scanner -c http://${DOCKER_SERVICE}:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true - } - - function code_quality() { - docker run --env SOURCE_CODE="$PWD" \ - --volume "$PWD":/code \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code - } - - function license_management() { - /run.sh analyze . - } - - function sast() { - case "$CI_SERVER_VERSION" in - *-ee) - - # Deprecation notice for CONFIDENCE_LEVEL variable - if [ -z "$SAST_CONFIDENCE_LEVEL" -a "$CONFIDENCE_LEVEL" ]; then - SAST_CONFIDENCE_LEVEL="$CONFIDENCE_LEVEL" - echo "WARNING: CONFIDENCE_LEVEL is deprecated and MUST be replaced with SAST_CONFIDENCE_LEVEL" - fi - - docker run --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" \ - --volume "$PWD:/code" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code - ;; - *) - echo "GitLab EE is required" - ;; - esac - } - - function dependency_scanning() { - case "$CI_SERVER_VERSION" in - *-ee) - docker run --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}" \ - --volume "$PWD:/code" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code - ;; - *) - echo "GitLab EE is required" - ;; - esac - } - - function get_replicas() { - track="${1:-stable}" - percentage="${2:-100}" - - env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' ) - env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' ) - - if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then - # for stable track get number of replicas from `PRODUCTION_REPLICAS` - eval new_replicas=\$${env_slug}_REPLICAS - if [[ -z "$new_replicas" ]]; then - new_replicas=$REPLICAS - fi - else - # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS` - eval new_replicas=\$${env_track}_${env_slug}_REPLICAS - if [[ -z "$new_replicas" ]]; then - eval new_replicas=\${env_track}_REPLICAS - fi - fi - - replicas="${new_replicas:-1}" - replicas="$(($replicas * $percentage / 100))" - - # always return at least one replicas - if [[ $replicas -gt 0 ]]; then - echo "$replicas" - else - echo 1 - fi - } - - # Extracts variables prefixed with K8S_SECRET_ - # and creates a Kubernetes secret. - # - # e.g. If we have the following environment variables: - # K8S_SECRET_A=value1 - # K8S_SECRET_B=multi\ word\ value - # - # Then we will create a secret with the following key-value pairs: - # data: - # A: dmFsdWUxCg== - # B: bXVsdGkgd29yZCB2YWx1ZQo= - function create_application_secret() { - track="${1-stable}" - export APPLICATION_SECRET_NAME=$(application_secret_name "$track") - - env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables - - kubectl create secret \ - -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \ - --from-env-file k8s_prefixed_variables -o yaml --dry-run | - kubectl replace -n "$KUBE_NAMESPACE" --force -f - - - export APPLICATION_SECRET_CHECKSUM=$(cat k8s_prefixed_variables | sha256sum | cut -d ' ' -f 1) - - rm k8s_prefixed_variables - } - - function deploy_name() { - name="$CI_ENVIRONMENT_SLUG" - track="${1-stable}" - - if [[ "$track" != "stable" ]]; then - name="$name-$track" - fi - - echo $name - } - - function application_secret_name() { - track="${1-stable}" - name=$(deploy_name "$track") - - echo "${name}-secret" - } - - function deploy() { - track="${1-stable}" - percentage="${2:-100}" - name=$(deploy_name "$track") - - replicas="1" - service_enabled="true" - postgres_enabled="$POSTGRES_ENABLED" - - # if track is different than stable, - # re-use all attached resources - if [[ "$track" != "stable" ]]; then - service_enabled="false" - postgres_enabled="false" - fi - - replicas=$(get_replicas "$track" "$percentage") - - if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then - secret_name='gitlab-registry' - else - secret_name='' - fi - - create_application_secret "$track" - - env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]') - eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS - if [ -n "$env_ADDITIONAL_HOSTS" ]; then - additional_hosts="{$env_ADDITIONAL_HOSTS}" - elif [ -n "$ADDITIONAL_HOSTS" ]; then - additional_hosts="{$ADDITIONAL_HOSTS}" - fi - - if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then - echo "Deploying first release with database initialization..." - helm upgrade --install \ - --wait \ - --set service.enabled="$service_enabled" \ - --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ - --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ - --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ - --set image.repository="$CI_APPLICATION_REPOSITORY" \ - --set image.tag="$CI_APPLICATION_TAG" \ - --set image.pullPolicy=IfNotPresent \ - --set image.secrets[0].name="$secret_name" \ - --set application.track="$track" \ - --set application.database_url="$DATABASE_URL" \ - --set application.secretName="$APPLICATION_SECRET_NAME" \ - --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ - --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ - --set service.url="$CI_ENVIRONMENT_URL" \ - --set service.additionalHosts="$additional_hosts" \ - --set replicaCount="$replicas" \ - --set postgresql.enabled="$postgres_enabled" \ - --set postgresql.nameOverride="postgres" \ - --set postgresql.postgresUser="$POSTGRES_USER" \ - --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \ - --set postgresql.postgresDatabase="$POSTGRES_DB" \ - --set postgresql.imageTag="$POSTGRES_VERSION" \ - --set application.initializeCommand="$DB_INITIALIZE" \ - --namespace="$KUBE_NAMESPACE" \ - "$name" \ - chart/ - - echo "Deploying second release..." - helm upgrade --reuse-values \ - --wait \ - --set application.initializeCommand="" \ - --set application.migrateCommand="$DB_MIGRATE" \ - --namespace="$KUBE_NAMESPACE" \ - "$name" \ - chart/ - else - echo "Deploying new release..." - helm upgrade --install \ - --wait \ - --set service.enabled="$service_enabled" \ - --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ - --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ - --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ - --set image.repository="$CI_APPLICATION_REPOSITORY" \ - --set image.tag="$CI_APPLICATION_TAG" \ - --set image.pullPolicy=IfNotPresent \ - --set image.secrets[0].name="$secret_name" \ - --set application.track="$track" \ - --set application.database_url="$DATABASE_URL" \ - --set application.secretName="$APPLICATION_SECRET_NAME" \ - --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ - --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ - --set service.url="$CI_ENVIRONMENT_URL" \ - --set service.additionalHosts="$additional_hosts" \ - --set replicaCount="$replicas" \ - --set postgresql.enabled="$postgres_enabled" \ - --set postgresql.nameOverride="postgres" \ - --set postgresql.postgresUser="$POSTGRES_USER" \ - --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \ - --set postgresql.postgresDatabase="$POSTGRES_DB" \ - --set application.migrateCommand="$DB_MIGRATE" \ - --namespace="$KUBE_NAMESPACE" \ - "$name" \ - chart/ - fi - - kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name" - } - - function scale() { - track="${1-stable}" - percentage="${2-100}" - name=$(deploy_name "$track") - - replicas=$(get_replicas "$track" "$percentage") - - if [[ -n "$(helm ls -q "^$name$")" ]]; then - helm upgrade --reuse-values \ - --wait \ - --set replicaCount="$replicas" \ - --namespace="$KUBE_NAMESPACE" \ - "$name" \ - chart/ - fi - } - - function install_dependencies() { - apk add -U openssl curl tar gzip bash ca-certificates git - curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub - curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk - apk add glibc-2.28-r0.apk - rm glibc-2.28-r0.apk - - curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx - mv linux-amd64/helm /usr/bin/ - mv linux-amd64/tiller /usr/bin/ - helm version --client - tiller -version - - curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" - chmod +x /usr/bin/kubectl - kubectl version --client - } - - # With the Kubernetes executor, 'localhost' must be used instead - # https://docs.gitlab.com/runner/executors/kubernetes.html - function setup_docker() { - if ! docker info &>/dev/null; then - if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then - export DOCKER_HOST='tcp://localhost:2375' - export DOCKER_SERVICE="localhost" - else - export DOCKER_SERVICE="docker" - fi - fi - } - - function setup_test_db() { - if [ -z ${KUBERNETES_PORT+x} ]; then - DB_HOST=postgres - else - DB_HOST=localhost - fi - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}" - } - - function download_chart() { - if [[ ! -d chart ]]; then - auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app} - auto_chart_name=$(basename $auto_chart) - auto_chart_name=${auto_chart_name%.tgz} - auto_chart_name=${auto_chart_name%.tar.gz} - else - auto_chart="chart" - auto_chart_name="chart" - fi - - helm init --client-only - helm repo add gitlab ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io} - if [[ ! -d "$auto_chart" ]]; then - helm fetch ${auto_chart} --untar - fi - if [ "$auto_chart_name" != "chart" ]; then - mv ${auto_chart_name} chart - fi - - helm dependency update chart/ - helm dependency build chart/ - } - - function ensure_namespace() { - kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE" - } - - - # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN - function ensure_kube_ingress_base_domain() { - if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ] && [ -n "$AUTO_DEVOPS_DOMAIN" ] ; then - export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN - fi - } - - function check_kube_domain() { - ensure_kube_ingress_base_domain - - if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then - echo "In order to deploy or use Review Apps," - echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set" - echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings" - echo "or by defining a variable at group or project level." - echo "You can also manually add it in .gitlab-ci.yml" - echo "AUTO_DEVOPS_DOMAIN support will be dropped on 12.0" - false - else - true - fi - } - - function initialize_tiller() { - echo "Checking Tiller..." - - export HELM_HOST="localhost:44134" - tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 & - echo "Tiller is listening on ${HELM_HOST}" - - if ! helm version --debug; then - echo "Failed to init Tiller." - return 1 - fi - echo "" - } - - function create_secret() { - echo "Create secret..." - if [[ "$CI_PROJECT_VISIBILITY" == "public" ]]; then - return - fi - - kubectl create secret -n "$KUBE_NAMESPACE" \ - docker-registry gitlab-registry \ - --docker-server="$CI_REGISTRY" \ - --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \ - --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \ - --docker-email="$GITLAB_USER_EMAIL" \ - -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f - - } - - function dast() { - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) - - mkdir /zap/wrk/ - /zap/zap-baseline.py -J gl-dast-report.json -t "$CI_ENVIRONMENT_URL" || true - cp /zap/wrk/gl-dast-report.json . - } - - function performance() { - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) - - mkdir gitlab-exporter - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js - - mkdir sitespeed-results - - if [ -f .gitlab-urls.txt ] - then - sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt - else - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" - fi - - mv sitespeed-results/data/performance.json performance.json - } - - function persist_environment_url() { - echo $CI_ENVIRONMENT_URL > environment_url.txt - } - - function delete() { - track="${1-stable}" - name=$(deploy_name "$track") - - if [[ -n "$(helm ls -q "^$name$")" ]]; then - helm delete --purge "$name" - fi - - secret_name=$(application_secret_name "$track") - kubectl delete secret --ignore-not-found -n "$KUBE_NAMESPACE" "$secret_name" - } - -before_script: - - *auto_devops +include: + - template: Jobs/Build.gitlab-ci.yml + - template: Jobs/Test.gitlab-ci.yml + - template: Jobs/Code-Quality.gitlab-ci.yml + - template: Jobs/Deploy.gitlab-ci.yml + - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml + - template: Jobs/DAST.gitlab-ci.yml + - template: Security/Container-Scanning.gitlab-ci.yml + - template: Security/Dependency-Scanning.gitlab-ci.yml + - template: Security/License-Management.gitlab-ci.yml + - template: Security/SAST.gitlab-ci.yml diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml new file mode 100644 index 00000000000..546c4affb4e --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml @@ -0,0 +1,38 @@ +performance: + stage: performance + image: docker:stable + allow_failure: true + services: + - docker:stable-dind + script: + - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) + - mkdir gitlab-exporter + - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js + - mkdir sitespeed-results + - | + if [ -f .gitlab-urls.txt ] + then + sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt + else + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" + fi + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - performance.json + - sitespeed-results/ + only: + refs: + - branches + - tags + kubernetes: active + except: + variables: + - $PERFORMANCE_DISABLED diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml new file mode 100644 index 00000000000..18f7290e1d9 --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -0,0 +1,18 @@ +build: + stage: build + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable" + services: + - docker:stable-dind + script: + - | + if [[ -z "$CI_COMMIT_TAG" ]]; then + export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG} + export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA} + else + export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE} + export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} + fi + - /build/build.sh + only: + - branches + - tags diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml new file mode 100644 index 00000000000..b09a24d8e22 --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -0,0 +1,27 @@ +code_quality: + stage: test + image: docker:stable + allow_failure: true + services: + - docker:stable-dind + script: + - export CQ_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') + - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - | + docker run --env SOURCE_CODE="$PWD" \ + --volume "$PWD":/code \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + "registry.gitlab.com/gitlab-org/security-products/codequality:$CQ_VERSION" /code + artifacts: + paths: [gl-code-quality-report.json] + only: + - branches + - tags + except: + variables: + - $CODE_QUALITY_DISABLED diff --git a/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml new file mode 100644 index 00000000000..aedbbb21674 --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml @@ -0,0 +1,54 @@ +dast: + stage: dast + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:stable-dind + script: + - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} + - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} + - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - | + function dast_run() { + docker run \ + --env DAST_TARGET_AVAILABILITY_TIMEOUT \ + --volume "$PWD:/output" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + -w /output \ + "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ + /analyze -t $DAST_WEBSITE \ + "$@" + } + - | + if [ -n "$DAST_AUTH_URL" ] + then + dast_run \ + --auth-url $DAST_AUTH_URL \ + --auth-username $DAST_USERNAME \ + --auth-password $DAST_PASSWORD \ + --auth-username-field $DAST_USERNAME_FIELD \ + --auth-password-field $DAST_PASSWORD_FIELD + else + dast_run + fi + artifacts: + reports: + dast: gl-dast-report.json + only: + refs: + - branches + - tags + variables: + - $GITLAB_FEATURES =~ /\bdast\b/ + except: + refs: + - master + variables: + - $DAST_DISABLED diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml new file mode 100644 index 00000000000..1e9591e113b --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -0,0 +1,575 @@ +review: + stage: review + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - initialize_tiller + - create_secret + - deploy + - persist_environment_url + environment: + name: review/$CI_COMMIT_REF_NAME + url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN + on_stop: stop_review + artifacts: + paths: [environment_url.txt] + only: + refs: + - branches + - tags + kubernetes: active + except: + refs: + - master + variables: + - $REVIEW_DISABLED + +stop_review: + stage: cleanup + variables: + GIT_STRATEGY: none + script: + - install_dependencies + - initialize_tiller + - delete + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + when: manual + allow_failure: true + only: + refs: + - branches + - tags + kubernetes: active + except: + refs: + - master + variables: + - $REVIEW_DISABLED + +# Staging deploys are disabled by default since +# continuous deployment to production is enabled by default +# If you prefer to automatically deploy to staging and +# only manually promote to production, enable this job by setting +# STAGING_ENABLED. + +staging: + stage: staging + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - initialize_tiller + - create_secret + - deploy + environment: + name: staging + url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN + only: + refs: + - master + kubernetes: active + variables: + - $STAGING_ENABLED + +# Canaries are disabled by default, but if you want them, +# and know what the downsides are, you can enable this by setting +# CANARY_ENABLED. + +canary: + stage: canary + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - initialize_tiller + - create_secret + - deploy canary + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN + when: manual + only: + refs: + - master + kubernetes: active + variables: + - $CANARY_ENABLED + +.production: &production_template + stage: production + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - initialize_tiller + - create_secret + - deploy + - delete canary + - delete rollout + - persist_environment_url + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN + artifacts: + paths: [environment_url.txt] + +production: + <<: *production_template + only: + refs: + - master + kubernetes: active + except: + variables: + - $STAGING_ENABLED + - $CANARY_ENABLED + - $INCREMENTAL_ROLLOUT_ENABLED + - $INCREMENTAL_ROLLOUT_MODE + +production_manual: + <<: *production_template + when: manual + allow_failure: false + only: + refs: + - master + kubernetes: active + variables: + - $STAGING_ENABLED + - $CANARY_ENABLED + except: + variables: + - $INCREMENTAL_ROLLOUT_ENABLED + - $INCREMENTAL_ROLLOUT_MODE + +# This job implements incremental rollout on for every push to `master`. + +.rollout: &rollout_template + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - initialize_tiller + - create_secret + - deploy rollout $ROLLOUT_PERCENTAGE + - scale stable $((100-ROLLOUT_PERCENTAGE)) + - delete canary + - persist_environment_url + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN + artifacts: + paths: [environment_url.txt] + +.manual_rollout_template: &manual_rollout_template + <<: *rollout_template + stage: production + when: manual + # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4) + only: + refs: + - master + kubernetes: active + variables: + - $INCREMENTAL_ROLLOUT_MODE == "manual" + - $INCREMENTAL_ROLLOUT_ENABLED + except: + variables: + - $INCREMENTAL_ROLLOUT_MODE == "timed" + +.timed_rollout_template: &timed_rollout_template + <<: *rollout_template + when: delayed + start_in: 5 minutes + only: + refs: + - master + kubernetes: active + variables: + - $INCREMENTAL_ROLLOUT_MODE == "timed" + +timed rollout 10%: + <<: *timed_rollout_template + stage: incremental rollout 10% + variables: + ROLLOUT_PERCENTAGE: 10 + +timed rollout 25%: + <<: *timed_rollout_template + stage: incremental rollout 25% + variables: + ROLLOUT_PERCENTAGE: 25 + +timed rollout 50%: + <<: *timed_rollout_template + stage: incremental rollout 50% + variables: + ROLLOUT_PERCENTAGE: 50 + +timed rollout 100%: + <<: *timed_rollout_template + <<: *production_template + stage: incremental rollout 100% + variables: + ROLLOUT_PERCENTAGE: 100 + +rollout 10%: + <<: *manual_rollout_template + variables: + ROLLOUT_PERCENTAGE: 10 + +rollout 25%: + <<: *manual_rollout_template + variables: + ROLLOUT_PERCENTAGE: 25 + +rollout 50%: + <<: *manual_rollout_template + variables: + ROLLOUT_PERCENTAGE: 50 + +rollout 100%: + <<: *manual_rollout_template + <<: *production_template + allow_failure: false + +.deploy_helpers: &deploy_helpers | + [[ "$TRACE" ]] && set -x + auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB} + export DATABASE_URL=${DATABASE_URL-$auto_database_url} + export TILLER_NAMESPACE=$KUBE_NAMESPACE + # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products + + function get_replicas() { + track="${1:-stable}" + percentage="${2:-100}" + + env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' ) + env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' ) + + if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then + # for stable track get number of replicas from `PRODUCTION_REPLICAS` + eval new_replicas=\$${env_slug}_REPLICAS + if [[ -z "$new_replicas" ]]; then + new_replicas=$REPLICAS + fi + else + # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS` + eval new_replicas=\$${env_track}_${env_slug}_REPLICAS + if [[ -z "$new_replicas" ]]; then + eval new_replicas=\${env_track}_REPLICAS + fi + fi + + replicas="${new_replicas:-1}" + replicas="$(($replicas * $percentage / 100))" + + # always return at least one replicas + if [[ $replicas -gt 0 ]]; then + echo "$replicas" + else + echo 1 + fi + } + + # Extracts variables prefixed with K8S_SECRET_ + # and creates a Kubernetes secret. + # + # e.g. If we have the following environment variables: + # K8S_SECRET_A=value1 + # K8S_SECRET_B=multi\ word\ value + # + # Then we will create a secret with the following key-value pairs: + # data: + # A: dmFsdWUxCg== + # B: bXVsdGkgd29yZCB2YWx1ZQo= + function create_application_secret() { + track="${1-stable}" + export APPLICATION_SECRET_NAME=$(application_secret_name "$track") + + env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables + + kubectl create secret \ + -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \ + --from-env-file k8s_prefixed_variables -o yaml --dry-run | + kubectl replace -n "$KUBE_NAMESPACE" --force -f - + + export APPLICATION_SECRET_CHECKSUM=$(cat k8s_prefixed_variables | sha256sum | cut -d ' ' -f 1) + + rm k8s_prefixed_variables + } + + function deploy_name() { + name="$CI_ENVIRONMENT_SLUG" + track="${1-stable}" + + if [[ "$track" != "stable" ]]; then + name="$name-$track" + fi + + echo $name + } + + function application_secret_name() { + track="${1-stable}" + name=$(deploy_name "$track") + + echo "${name}-secret" + } + + function deploy() { + track="${1-stable}" + percentage="${2:-100}" + name=$(deploy_name "$track") + + if [[ -z "$CI_COMMIT_TAG" ]]; then + image_repository=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG} + image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA} + else + image_repository=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE} + image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} + fi + + replicas="1" + service_enabled="true" + postgres_enabled="$POSTGRES_ENABLED" + + # if track is different than stable, + # re-use all attached resources + if [[ "$track" != "stable" ]]; then + service_enabled="false" + postgres_enabled="false" + fi + + replicas=$(get_replicas "$track" "$percentage") + + if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then + secret_name='gitlab-registry' + else + secret_name='' + fi + + create_application_secret "$track" + + env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]') + eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS + if [ -n "$env_ADDITIONAL_HOSTS" ]; then + additional_hosts="{$env_ADDITIONAL_HOSTS}" + elif [ -n "$ADDITIONAL_HOSTS" ]; then + additional_hosts="{$ADDITIONAL_HOSTS}" + fi + + if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then + echo "Deploying first release with database initialization..." + helm upgrade --install \ + --wait \ + --set service.enabled="$service_enabled" \ + --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ + --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ + --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ + --set image.repository="$image_repository" \ + --set image.tag="$image_tag" \ + --set image.pullPolicy=IfNotPresent \ + --set image.secrets[0].name="$secret_name" \ + --set application.track="$track" \ + --set application.database_url="$DATABASE_URL" \ + --set application.secretName="$APPLICATION_SECRET_NAME" \ + --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ + --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ + --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ + --set replicaCount="$replicas" \ + --set postgresql.enabled="$postgres_enabled" \ + --set postgresql.nameOverride="postgres" \ + --set postgresql.postgresUser="$POSTGRES_USER" \ + --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \ + --set postgresql.postgresDatabase="$POSTGRES_DB" \ + --set postgresql.imageTag="$POSTGRES_VERSION" \ + --set application.initializeCommand="$DB_INITIALIZE" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + + echo "Deploying second release..." + helm upgrade --reuse-values \ + --wait \ + --set application.initializeCommand="" \ + --set application.migrateCommand="$DB_MIGRATE" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + else + echo "Deploying new release..." + helm upgrade --install \ + --wait \ + --set service.enabled="$service_enabled" \ + --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ + --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ + --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ + --set image.repository="$image_repository" \ + --set image.tag="$image_tag" \ + --set image.pullPolicy=IfNotPresent \ + --set image.secrets[0].name="$secret_name" \ + --set application.track="$track" \ + --set application.database_url="$DATABASE_URL" \ + --set application.secretName="$APPLICATION_SECRET_NAME" \ + --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ + --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ + --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ + --set replicaCount="$replicas" \ + --set postgresql.enabled="$postgres_enabled" \ + --set postgresql.nameOverride="postgres" \ + --set postgresql.postgresUser="$POSTGRES_USER" \ + --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \ + --set postgresql.postgresDatabase="$POSTGRES_DB" \ + --set application.migrateCommand="$DB_MIGRATE" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + fi + + kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name" + } + + function scale() { + track="${1-stable}" + percentage="${2-100}" + name=$(deploy_name "$track") + + replicas=$(get_replicas "$track" "$percentage") + + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm upgrade --reuse-values \ + --wait \ + --set replicaCount="$replicas" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + fi + } + + function install_dependencies() { + apk add -U openssl curl tar gzip bash ca-certificates git + curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub + curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk + apk add glibc-2.28-r0.apk + rm glibc-2.28-r0.apk + + curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx + mv linux-amd64/helm /usr/bin/ + mv linux-amd64/tiller /usr/bin/ + helm version --client + tiller -version + + curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" + chmod +x /usr/bin/kubectl + kubectl version --client + } + + function download_chart() { + if [[ ! -d chart ]]; then + auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app} + auto_chart_name=$(basename $auto_chart) + auto_chart_name=${auto_chart_name%.tgz} + auto_chart_name=${auto_chart_name%.tar.gz} + else + auto_chart="chart" + auto_chart_name="chart" + fi + + helm init --client-only + helm repo add gitlab ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io} + if [[ ! -d "$auto_chart" ]]; then + helm fetch ${auto_chart} --untar + fi + if [ "$auto_chart_name" != "chart" ]; then + mv ${auto_chart_name} chart + fi + + helm dependency update chart/ + helm dependency build chart/ + } + + function ensure_namespace() { + kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE" + } + + # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN + function ensure_kube_ingress_base_domain() { + if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ] && [ -n "$AUTO_DEVOPS_DOMAIN" ] ; then + export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN + fi + } + + function check_kube_domain() { + ensure_kube_ingress_base_domain + + if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then + echo "In order to deploy or use Review Apps," + echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set" + echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings" + echo "or by defining a variable at group or project level." + echo "You can also manually add it in .gitlab-ci.yml" + echo "AUTO_DEVOPS_DOMAIN support will be dropped on 12.0" + false + else + true + fi + } + + function initialize_tiller() { + echo "Checking Tiller..." + + export HELM_HOST="localhost:44134" + tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 & + echo "Tiller is listening on ${HELM_HOST}" + + if ! helm version --debug; then + echo "Failed to init Tiller." + return 1 + fi + echo "" + } + + function create_secret() { + echo "Create secret..." + if [[ "$CI_PROJECT_VISIBILITY" == "public" ]]; then + return + fi + + kubectl create secret -n "$KUBE_NAMESPACE" \ + docker-registry gitlab-registry \ + --docker-server="$CI_REGISTRY" \ + --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \ + --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \ + --docker-email="$GITLAB_USER_EMAIL" \ + -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f - + } + + function persist_environment_url() { + echo $CI_ENVIRONMENT_URL > environment_url.txt + } + + function delete() { + track="${1-stable}" + name=$(deploy_name "$track") + + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm delete --purge "$name" + fi + + secret_name=$(application_secret_name "$track") + kubectl delete secret --ignore-not-found -n "$KUBE_NAMESPACE" "$secret_name" + } + +before_script: + - *deploy_helpers diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml new file mode 100644 index 00000000000..b9fe838d1da --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml @@ -0,0 +1,23 @@ +test: + services: + - postgres:latest + variables: + POSTGRES_DB: test + stage: test + image: gliderlabs/herokuish:latest + script: + - | + if [ -z ${KUBERNETES_PORT+x} ]; then + DB_HOST=postgres + else + DB_HOST=localhost + fi + - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}" + - cp -R . /tmp/app + - /bin/herokuish buildpack test + only: + - branches + - tags + except: + variables: + - $TEST_DISABLED diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml index 0b7a531682b..eef361c19e9 100644 --- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml @@ -28,6 +28,12 @@ container_scanning: - docker:stable-dind script: - if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then { export DOCKER_SERVICE="localhost" ; export DOCKER_HOST="tcp://${DOCKER_SERVICE}:2375" ; } fi + - | + if [[ -n "$CI_REGISTRY_USER" ]]; then + echo "Logging to GitLab Container Registry with CI credentials..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + echo "" + fi - docker run -d --name db arminc/clair-db:latest - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION} - apk add -U wget ca-certificates @@ -36,7 +42,6 @@ container_scanning: - mv clair-scanner_linux_amd64 clair-scanner - chmod +x clair-scanner - touch clair-whitelist.yml - - while( ! wget -q -O /dev/null http://${DOCKER_SERVICE}:6060/v1/namespaces ) ; do sleep 1 ; done - retries=0 - echo "Waiting for clair daemon to start" - while( ! wget -T 10 -q -O /dev/null http://${DOCKER_SERVICE}:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml index ef6d7866e85..770340de16f 100644 --- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml @@ -4,6 +4,9 @@ # List of the variables: https://gitlab.com/gitlab-org/security-products/dast#settings # How to set: https://docs.gitlab.com/ee/ci/yaml/#variables +include: + - template: Jobs/DAST.gitlab-ci.yml + variables: DAST_WEBSITE: http://example.com # Please edit to be your website to scan for vulnerabilities @@ -14,46 +17,10 @@ stages: - dast dast: - stage: dast - image: docker:stable - variables: - DOCKER_DRIVER: overlay2 - allow_failure: true - services: - - docker:stable-dind - script: - - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} - - | - function dast_run() { - docker run \ - --env DAST_TARGET_AVAILABILITY_TIMEOUT \ - --volume "$PWD:/output" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - -w /output \ - "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ - /analyze -t $DAST_WEBSITE \ - "$@" - } - - | - if [ -n "$DAST_AUTH_URL" ] - then - dast_run \ - --auth-url $DAST_AUTH_URL \ - --auth-username $DAST_USERNAME \ - --auth-password $DAST_PASSWORD \ - --auth-username-field $DAST_USERNAME_FIELD \ - --auth-password-field $DAST_PASSWORD_FIELD - else - dast_run - fi - artifacts: - reports: - dast: gl-dast-report.json only: refs: - branches - variables: - - $GITLAB_FEATURES =~ /\bdast\b/ except: + refs: [] # Override default from template variables: - $DAST_DISABLED diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index fd666541d41..7f80a6e9285 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -15,6 +15,12 @@ dependency_scanning: script: - export DS_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - | docker run \ --env DS_ANALYZER_IMAGES \ --env DS_ANALYZER_IMAGE_PREFIX \ diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 034fba5499c..b941e89991e 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -15,6 +15,12 @@ sast: script: - export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - | docker run \ --env SAST_ANALYZER_IMAGES \ --env SAST_ANALYZER_IMAGE_PREFIX \ diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb index 37ba16dba25..6667a5d3d33 100644 --- a/lib/gitlab/config/entry/configurable.rb +++ b/lib/gitlab/config/entry/configurable.rb @@ -21,7 +21,7 @@ module Gitlab include Validatable validations do - validates :config, type: Hash + validates :config, type: Hash, unless: :skip_config_hash_validation? end end @@ -30,6 +30,10 @@ module Gitlab return unless valid? self.class.nodes.each do |key, factory| + # If we override the config type validation + # we can end with different config types like String + next unless config.is_a?(Hash) + factory .value(config[key]) .with(key: key, parent: self) @@ -45,6 +49,10 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord + def skip_config_hash_validation? + false + end + class_methods do def nodes Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb index 79f9ff32514..3c06b1e0d24 100644 --- a/lib/gitlab/config/entry/factory.rb +++ b/lib/gitlab/config/entry/factory.rb @@ -61,7 +61,7 @@ module Gitlab end def fabricate(entry, value = nil) - entry.new(value, @metadata).tap do |node| + entry.new(value, @metadata) do |node| node.key = @attributes[:key] node.parent = @attributes[:parent] node.default = @attributes[:default] diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb index 9999ab4ff95..e014f15fbd8 100644 --- a/lib/gitlab/config/entry/node.rb +++ b/lib/gitlab/config/entry/node.rb @@ -17,6 +17,8 @@ module Gitlab @metadata = metadata @entries = {} + yield(self) if block_given? + self.class.aspects.to_a.each do |aspect| instance_exec(&aspect) end @@ -44,6 +46,12 @@ module Gitlab @parent ? @parent.ancestors + [@parent] : [] end + def opt(key) + opt = metadata[key] + opt = @parent.opt(key) if opt.nil? && @parent + opt + end + def valid? errors.none? end @@ -85,6 +93,18 @@ module Gitlab "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" end + def hash? + @config.is_a?(Hash) + end + + def string? + @config.is_a?(String) + end + + def integer? + @config.is_a?(Integer) + end + def self.default(**) end diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb index 5fbf7565e2a..a56a89adb35 100644 --- a/lib/gitlab/config/entry/simplifiable.rb +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -19,7 +19,10 @@ module Gitlab entry = self.class.entry_class(strategy) - super(@subject = entry.new(config, metadata)) + @subject = entry.new(config, metadata) + + yield(@subject) if block_given? + super(@subject) end def self.strategy(name, **opts) diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index d348e11b753..d0ee94370ba 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -15,6 +15,17 @@ module Gitlab end end + class DisallowedKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + present_keys = value.try(:keys).to_a & options[:in] + + if present_keys.any? + record.errors.add(attribute, "contains disallowed keys: " + + present_keys.join(', ')) + end + end + end + class AllowedValuesValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless options[:in].include?(value.to_s) @@ -186,6 +197,97 @@ module Gitlab end end end + + class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return unless value.is_a?(Array) + + ports_size = value.count + return if ports_size <= 1 + + named_ports = value.select { |e| e.is_a?(Hash) }.map { |e| e[:name] }.compact.map(&:downcase) + + if ports_size != named_ports.size + record.errors.add(attribute, 'when there is more than one port, a unique name should be added') + end + + if ports_size != named_ports.uniq.size + record.errors.add(attribute, 'each port name must be different') + end + end + end + + class PortUniqueValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + value = ports(value) + return unless value.is_a?(Array) + + ports_size = value.count + return if ports_size <= 1 + + if transform_ports(value).size != ports_size + record.errors.add(attribute, 'each port number can only be referenced once') + end + end + + private + + def ports(current_data) + current_data + end + + def transform_ports(raw_ports) + raw_ports.map do |port| + case port + when Integer + port + when Hash + port[:number] + end + end.uniq + end + end + + class JobPortUniqueValidator < PortUniqueValidator + private + + def ports(current_data) + return unless current_data.is_a?(Hash) + + (image_ports(current_data) + services_ports(current_data)).compact + end + + def image_ports(current_data) + return [] unless current_data[:image].is_a?(Hash) + + current_data.dig(:image, :ports).to_a + end + + def services_ports(current_data) + current_data.dig(:services).to_a.flat_map { |service| service.is_a?(Hash) ? service[:ports] : nil } + end + end + + class ServicesWithPortsAliasUniqueValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + current_aliases = aliases(value) + return if current_aliases.empty? + + unless aliases_unique?(current_aliases) + record.errors.add(:config, 'alias must be unique in services with ports') + end + end + + private + + def aliases(value) + value.select { |s| s.is_a?(Hash) && s[:ports] }.pluck(:alias) # rubocop:disable CodeReuse/ActiveRecord + end + + def aliases_unique?(aliases) + aliases.size == aliases.uniq.size + end + end end end end diff --git a/lib/gitlab/graphql/tracing.rb b/lib/gitlab/graphql/tracing.rb deleted file mode 100644 index 6b505e4262b..00000000000 --- a/lib/gitlab/graphql/tracing.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - class Tracing < GraphQL::Tracing::PlatformTracing - self.platform_keys = { - 'lex' => 'graphql.lex', - 'parse' => 'graphql.parse', - 'validate' => 'graphql.validate', - 'analyze_query' => 'graphql.analyze', - 'analyze_multiplex' => 'graphql.analyze', - 'execute_multiplex' => 'graphql.execute', - 'execute_query' => 'graphql.execute', - 'execute_query_lazy' => 'graphql.execute', - 'execute_field' => 'graphql.execute', - 'execute_field_lazy' => 'graphql.execute' - } - - def platform_field_key(type, field) - "#{type.name}.#{field.name}" - end - - def platform_trace(platform_key, key, data, &block) - start = Gitlab::Metrics::System.monotonic_time - - yield - ensure - duration = Gitlab::Metrics::System.monotonic_time - start - - graphql_duration_seconds.observe({ platform_key: platform_key, key: key }, duration) - end - - private - - def graphql_duration_seconds - @graphql_duration_seconds ||= Gitlab::Metrics.histogram( - :graphql_duration_seconds, - 'GraphQL execution time' - ) - end - end - end -end diff --git a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb index 34b705138ba..c49877ddf9d 100644 --- a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb +++ b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb @@ -7,12 +7,16 @@ module Gitlab include QueryAdditionalMetrics # rubocop: disable CodeReuse/ActiveRecord - def query(environment_id) + def query(environment_id, timeframe_start = 8.hours.ago, timeframe_end = Time.now) ::Environment.find_by(id: environment_id).try do |environment| query_metrics( environment.project, environment, - common_query_context(environment, timeframe_start: 8.hours.ago.to_f, timeframe_end: Time.now.to_f) + common_query_context( + environment, + timeframe_start: timeframe_start.to_f, + timeframe_end: timeframe_end.to_f + ) ) end end diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index 14126b6ec06..c237f4a7404 100644 --- a/lib/gitlab/untrusted_regexp.rb +++ b/lib/gitlab/untrusted_regexp.rb @@ -47,6 +47,19 @@ module Gitlab self.source == other.source end + # Handles regular expressions with the preferred RE2 library where possible + # via UntustedRegex. Falls back to Ruby's built-in regular expression library + # when the syntax would be invalid in RE2. + # + # One difference between these is `(?m)` multi-line mode. Ruby regex enables + # this by default, but also handles `^` and `$` differently. + # See: https://www.regular-expressions.info/modifiers.html + def self.with_fallback(pattern, multiline: false) + UntrustedRegexp.new(pattern, multiline: multiline) + rescue RegexpError + Regexp.new(pattern) + end + private attr_reader :regexp diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index 7872e5b08c0..8fadadccce9 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -14,6 +14,12 @@ namespace :gitlab do rake_version = run_and_match(%w(rake --version), /[\d\.]+/).try(:to_s) # check redis version redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a + + # check for system defined proxies + if Gitlab.ee? + proxies = Gitlab::Proxy.detect_proxy.map {|k, v| "#{k}: #{v}"}.join("\n\t\t") + end + # check Git version git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a # check Go version @@ -22,6 +28,11 @@ namespace :gitlab do puts "" puts "System information".color(:yellow) puts "System:\t\t#{os_name || "unknown".color(:red)}" + + if Gitlab.ee? + puts "Proxy:\t\t#{proxies.present? ? proxies.color(:green) : "no"}" + end + puts "Current User:\t#{run_command(%w(whoami))}" puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}" puts "RVM Version:\t#{rvm_version}" if rvm_version.present? @@ -39,6 +50,15 @@ namespace :gitlab do http_clone_url = project.http_url_to_repo ssh_clone_url = project.ssh_url_to_repo + if Gitlab.ee? + geo_node_type = + if Gitlab::Geo.current_node + Gitlab::Geo.current_node.primary ? 'Primary' : 'Secondary' + else + 'Undefined'.color(:red) + end + end + omniauth_providers = Gitlab.config.omniauth.providers.map { |provider| provider['name'] } puts "" @@ -51,6 +71,13 @@ namespace :gitlab do puts "URL:\t\t#{Gitlab.config.gitlab.url}" puts "HTTP Clone URL:\t#{http_clone_url}" puts "SSH Clone URL:\t#{ssh_clone_url}" + + if Gitlab.ee? + puts "Elasticsearch:\t#{Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing? ? "yes".color(:green) : "no"}" + puts "Geo:\t\t#{Gitlab::Geo.enabled? ? "yes".color(:green) : "no"}" + puts "Geo node:\t#{geo_node_type}" if Gitlab::Geo.enabled? + end + puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}" puts "Using Omniauth:\t#{Gitlab::Auth.omniauth_enabled? ? "yes".color(:green) : "no"}" puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab::Auth.omniauth_enabled? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fcbd34a05d5..f9c411642c7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1620,6 +1620,9 @@ msgstr "" msgid "CiVariables|Remove variable row" msgstr "" +msgid "CiVariables|This variable will not be masked" +msgstr "" + msgid "CiVariable|* (All environments)" msgstr "" @@ -1629,9 +1632,15 @@ msgstr "" msgid "CiVariable|Error occurred while saving variables" msgstr "" +msgid "CiVariable|Masked" +msgstr "" + msgid "CiVariable|Protected" msgstr "" +msgid "CiVariable|Toggle masked" +msgstr "" + msgid "CiVariable|Toggle protected" msgstr "" @@ -3181,10 +3190,7 @@ msgstr "" msgid "Enter the merge request title" msgstr "" -msgid "Environment variables" -msgstr "" - -msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want." +msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want." msgstr "" msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default" @@ -3595,6 +3601,9 @@ msgstr "" msgid "Failed to remove user key." msgstr "" +msgid "Failed to save new settings" +msgstr "" + msgid "Failed to update issues, please try again." msgstr "" @@ -8278,9 +8287,6 @@ msgstr "" msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}" msgstr "" -msgid "This source diff could not be displayed because it is too large." -msgstr "" - msgid "This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like \"1 hour\". Values without specification represent seconds." msgstr "" @@ -8353,9 +8359,6 @@ msgstr "" msgid "Timeago|%s months remaining" msgstr "" -msgid "Timeago|%s seconds ago" -msgstr "" - msgid "Timeago|%s seconds remaining" msgstr "" @@ -8948,6 +8951,9 @@ msgstr "" msgid "Value" msgstr "" +msgid "Variables" +msgstr "" + msgid "Various container registry settings." msgstr "" diff --git a/package.json b/package.json index a2d96cc551a..ceb36a92cc8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .", "jest": "BABEL_ENV=jest jest", + "jest-debug": "BABEL_ENV=jest node --inspect-brk node_modules/.bin/jest --runInBand", "jsdoc": "jsdoc -c config/jsdocs.config.js", "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js", "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", @@ -32,7 +33,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.9.0", - "@gitlab/svgs": "^1.57.0", + "@gitlab/svgs": "^1.58.0", "@gitlab/ui": "^3.0.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", @@ -56,11 +57,13 @@ "d3-array": "^1.2.1", "d3-axis": "^1.0.8", "d3-brush": "^1.0.4", + "d3-ease": "^1.0.3", "d3-scale": "^1.0.7", "d3-selection": "^1.2.0", "d3-shape": "^1.2.0", "d3-time": "^1.0.8", "d3-time-format": "^2.1.1", + "d3-transition": "^1.1.1", "dateformat": "^3.0.3", "deckar01-task_list": "^2.2.0", "diff": "^3.4.0", diff --git a/qa/STYLE_GUIDE.md b/qa/STYLE_GUIDE.md index f85e7492409..900f7456e1a 100644 --- a/qa/STYLE_GUIDE.md +++ b/qa/STYLE_GUIDE.md @@ -43,4 +43,4 @@ end Notice that in the above example, before clicking the `:operations_environments_link`, another element is hovered over. -> We can create these methods as helpers to abstrac multi-step navigation.
\ No newline at end of file +> We can create these methods as helpers to abstract multi-step navigation.
\ No newline at end of file diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 36ce1119100..2dca2c3976f 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -392,7 +392,7 @@ describe Projects::EnvironmentsController do context 'when requesting metrics as JSON' do it 'returns a metrics JSON document' do - get :additional_metrics, params: environment_params(format: :json) + additional_metrics expect(response).to have_gitlab_http_status(204) expect(json_response).to eq({}) @@ -412,7 +412,7 @@ describe Projects::EnvironmentsController do end it 'returns a metrics JSON document' do - get :additional_metrics, params: environment_params(format: :json) + additional_metrics expect(response).to be_ok expect(json_response['success']).to be(true) @@ -420,6 +420,32 @@ describe Projects::EnvironmentsController do expect(json_response['last_update']).to eq(42) end end + + context 'when only one time param is provided' do + context 'when :metrics_time_window feature flag is disabled' do + before do + stub_feature_flags(metrics_time_window: false) + expect(environment).to receive(:additional_metrics).with(no_args).and_return(nil) + end + + it 'returns a time-window agnostic response' do + additional_metrics(start: '1552647300.651094') + + expect(response).to have_gitlab_http_status(204) + expect(json_response).to eq({}) + end + end + + it 'raises an error when start is missing' do + expect { additional_metrics(start: '1552647300.651094') } + .to raise_error(ActionController::ParameterMissing) + end + + it 'raises an error when end is missing' do + expect { additional_metrics(start: '1552647300.651094') } + .to raise_error(ActionController::ParameterMissing) + end + end end describe 'GET #search' do @@ -500,4 +526,8 @@ describe Projects::EnvironmentsController do project_id: project, id: environment.id) end + + def additional_metrics(opts = {}) + get :additional_metrics, params: environment_params(format: :json, **opts) + end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index ece8532cb84..b64ae552efc 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -28,6 +28,8 @@ describe Projects::PipelinesController do end it 'returns serialized pipelines', :request_store do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + queries = ActiveRecord::QueryRecorder.new do get_pipelines_index_json end diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb index 1a53e7c9512..fc5777e8c7c 100644 --- a/spec/features/group_variables_spec.rb +++ b/spec/features/group_variables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Group variables', :js do let(:user) { create(:user) } let(:group) { create(:group) } - let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', group: group) } + let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', masked: true, group: group) } let(:page_path) { group_settings_ci_cd_path(group) } before do diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index dc0862be6fc..e5770905dbd 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -67,18 +67,7 @@ describe 'Merge request > User posts notes', :js do end end - describe 'when reply_to_individual_notes feature flag is disabled' do - before do - stub_feature_flags(reply_to_individual_notes: false) - visit project_merge_request_path(project, merge_request) - end - - it 'does not show a reply button' do - expect(page).to have_no_selector('.js-reply-button') - end - end - - describe 'when reply_to_individual_notes feature flag is not set' do + describe 'reply button' do before do visit project_merge_request_path(project, merge_request) end diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb index 6bdf5df1036..76abc640077 100644 --- a/spec/features/project_variables_spec.rb +++ b/spec/features/project_variables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Project variables', :js do let(:user) { create(:user) } let(:project) { create(:project) } - let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value') } + let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value', masked: true) } let(:page_path) { project_settings_ci_cd_path(project) } before do diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb index 3b48ea4786d..d2cdade88d1 100644 --- a/spec/features/user_sees_revert_modal_spec.rb +++ b/spec/features/user_sees_revert_modal_spec.rb @@ -17,12 +17,14 @@ describe 'Merge request > User sees revert modal', :js do end it 'shows the revert modal' do - expect(page).to have_content('Revert this merge request') + page.within('.modal-header') do + expect(page).to have_content 'Revert this merge request' + end end it 'closes the revert modal with escape keypress' do find('#modal-revert-commit').send_keys(:escape) - expect(page).not_to have_content('Revert this merge request') + expect(page).not_to have_selector('#modal-revert-commit', visible: true) end end diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json index cd50be00418..918f2c4b47d 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json @@ -119,6 +119,12 @@ "merge_status", "sha", "merge_commit_sha", "user_notes_count", "should_remove_source_branch", "force_remove_source_branch", "web_url", "squash" - ] + ], + "head_pipeline": { + "oneOf": [ + { "type": "null" }, + { "$ref": "pipeline/detail.json" } + ] + } } } diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json index 56f86856dd4..a7207d2d991 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json @@ -13,6 +13,5 @@ "ref": { "type": "string" }, "status": { "type": "string" }, "web_url": { "type": "string" } - }, - "additionalProperties": false + } } diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json new file mode 100644 index 00000000000..63e130d4055 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "basic.json" }, + { + "properties": { + "before_sha": { "type": ["string", "null"] }, + "tag": { "type": ["boolean"] }, + "yaml_errors": { "type": ["string", "null"] }, + "user": { + "anyOf": [ + { "type": ["object", "null"] }, + { "$ref": "../user/basic.json" } + ] + }, + "created_at": { "type": ["date", "null"] }, + "updated_at": { "type": ["date", "null"] }, + "started_at": { "type": ["date", "null"] }, + "finished_at": { "type": ["date", "null"] }, + "committed_at": { "type": ["date", "null"] }, + "duration": { "type": ["number", "null"] }, + "coverage": { "type": ["string", "null"] }, + "detailed_status": { + "oneOf": [ + { "type": "null" }, + { "$ref": "../../../status/ci_detailed_status.json" } + ] + } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json index 6f6b044115b..305071a6b3f 100644 --- a/spec/fixtures/api/schemas/variable.json +++ b/spec/fixtures/api/schemas/variable.json @@ -4,12 +4,14 @@ "id", "key", "value", + "masked", "protected" ], "properties": { "id": { "type": "integer" }, "key": { "type": "string" }, "value": { "type": "string" }, + "masked": { "type": "boolean" }, "protected": { "type": "boolean" }, "environment_scope": { "type": "string", "optional": true } }, diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js index fe791aa2b74..aa1fa0373db 100644 --- a/spec/frontend/ide/lib/files_spec.js +++ b/spec/frontend/ide/lib/files_spec.js @@ -1,5 +1,5 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; -import { decorateFiles, splitParent } from '~/ide/lib/files'; +import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files'; import { decorateData } from '~/ide/stores/utils'; const TEST_BRANCH_ID = 'lorem-ipsum'; @@ -20,7 +20,7 @@ const createEntries = paths => { id: path, name, path, - url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${path}`), + url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${escapeFileUrl(path)}`), type, previewMode: viewerInformationForPath(path), parentPath: parent, @@ -28,7 +28,7 @@ const createEntries = paths => { ? parentEntry.url : createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}`), }), - tree: children.map(childName => jasmine.objectContaining({ name: childName })), + tree: children.map(childName => expect.objectContaining({ name: childName })), }; return acc; @@ -36,10 +36,10 @@ const createEntries = paths => { const entries = paths.reduce(createEntry, {}); - // Wrap entries in jasmine.objectContaining. + // Wrap entries in expect.objectContaining. // We couldn't do this earlier because we still need to select properties from parent entries. return Object.keys(entries).reduce((acc, key) => { - acc[key] = jasmine.objectContaining(entries[key]); + acc[key] = expect.objectContaining(entries[key]); return acc; }, {}); @@ -47,13 +47,14 @@ const createEntries = paths => { describe('IDE lib decorate files', () => { it('creates entries and treeList', () => { - const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'README.md']; + const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'app/#weird#file?.txt', 'README.md']; const expectedEntries = createEntries([ - { path: 'app', type: 'tree', children: ['assets', 'bugs.js'] }, + { path: 'app', type: 'tree', children: ['assets', '#weird#file?.txt', 'bugs.js'] }, { path: 'app/assets', type: 'tree', children: ['apples'] }, { path: 'app/assets/apples', type: 'tree', children: ['foo.js'] }, { path: 'app/assets/apples/foo.js', type: 'blob', children: [] }, { path: 'app/bugs.js', type: 'blob', children: [] }, + { path: 'app/#weird#file?.txt', type: 'blob', children: [] }, { path: 'README.md', type: 'blob', children: [] }, ]); @@ -64,7 +65,7 @@ describe('IDE lib decorate files', () => { }); // Here we test the keys and then each key/value individually because `expect(entries).toEqual(expectedEntries)` - // was taking a very long time for some reason. Probably due to large objects and nested `jasmine.objectContaining`. + // was taking a very long time for some reason. Probably due to large objects and nested `expect.objectContaining`. const entryKeys = Object.keys(entries); expect(entryKeys).toEqual(Object.keys(expectedEntries)); diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 2bc3933809f..6808ed86c9a 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -230,5 +230,18 @@ describe BlobHelper do expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master") end + + it 'escapes special characters' do + Rails.application.routes.default_url_options[:script_name] = nil + + expect(helper.ide_edit_path(project, "testing/#hashes", "readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/readme.md%23test") + expect(helper.ide_edit_path(project, "testing/#hashes", "src#/readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/src%23/readme.md%23test") + end + + it 'does not escape "/" character' do + Rails.application.routes.default_url_options[:script_name] = nil + + expect(helper.ide_edit_path(project, "testing/slashes", "readme.md/")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md/") + end end end diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index 6e6b3e6950b..d08ee41802b 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -103,4 +103,18 @@ describe('Board component', () => { }) .catch(done.fail); }); + + it('does render add issue button', () => { + expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull(); + }); + + it('does not render add issue button when list type is blank', done => { + vm.list.type = 'blank'; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.issue-count-badge-add-button')).toBeNull(); + + done(); + }); + }); }); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 70f49469300..394e60fc22c 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -127,20 +127,25 @@ describe('VariableList', () => { variableList.init(); }); - it('should add another row when editing the last rows protected checkbox', done => { + it('should not add another row when editing the last rows protected checkbox', done => { const $row = $wrapper.find('.js-row:last-child'); $row.find('.ci-variable-protected-item .js-project-feature-toggle').click(); getSetTimeoutPromise() .then(() => { - expect($wrapper.find('.js-row').length).toBe(2); + expect($wrapper.find('.js-row').length).toBe(1); + }) + .then(done) + .catch(done.fail); + }); - // Check for the correct default in the new row - const $protectedInput = $wrapper - .find('.js-row:last-child') - .find('.js-ci-variable-input-protected'); + it('should not add another row when editing the last rows masked checkbox', done => { + const $row = $wrapper.find('.js-row:last-child'); + $row.find('.ci-variable-masked-item .js-project-feature-toggle').click(); - expect($protectedInput.val()).toBe('false'); + getSetTimeoutPromise() + .then(() => { + expect($wrapper.find('.js-row').length).toBe(1); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index d9b298e84da..ef4589ada48 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -141,18 +141,16 @@ describe('DiffFile', () => { it('should have too large warning and blob link', done => { const BLOB_LINK = '/file/view/path'; vm.file.viewer.error = diffViewerErrors.too_large; + vm.file.viewer.error_message = + 'This source diff could not be displayed because it is too large'; vm.file.view_path = BLOB_LINK; + vm.file.renderIt = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain( 'This source diff could not be displayed because it is too large', ); - expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined(); - expect( - vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK), - ).toBeGreaterThan(-1); - done(); }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index d604e90b529..0cfcc994234 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -128,87 +128,33 @@ describe('noteActions', () => { }); }); - describe('with feature flag replyToIndividualNotes enabled', () => { + describe('for showReply = true', () => { beforeEach(() => { - gon.features = { - replyToIndividualNotes: true, - }; - }); - - afterEach(() => { - gon.features = {}; - }); - - describe('for showReply = true', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: true, - }); - }); - - it('shows a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); - - expect(replyButton.exists()).toBe(true); + wrapper = shallowMountNoteActions({ + ...props, + showReply: true, }); }); - describe('for showReply = false', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: false, - }); - }); - - it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); + it('shows a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); - expect(replyButton.exists()).toBe(false); - }); + expect(replyButton.exists()).toBe(true); }); }); - describe('with feature flag replyToIndividualNotes disabled', () => { + describe('for showReply = false', () => { beforeEach(() => { - gon.features = { - replyToIndividualNotes: false, - }; - }); - - afterEach(() => { - gon.features = {}; - }); - - describe('for showReply = true', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: true, - }); - }); - - it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); - - expect(replyButton.exists()).toBe(false); + wrapper = shallowMountNoteActions({ + ...props, + showReply: false, }); }); - describe('for showReply = false', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: false, - }); - }); - - it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); + it('does not show a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); - expect(replyButton.exists()).toBe(false); - }); + expect(replyButton.exists()).toBe(false); }); }); }); diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js new file mode 100644 index 00000000000..a89952ee435 --- /dev/null +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js @@ -0,0 +1,167 @@ +import $ from 'jquery'; +import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars +import TimezoneDropdown, { + formatUtcOffset, + formatTimezone, +} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; + +describe('Timezone Dropdown', function() { + preloadFixtures('pipeline_schedules/edit.html'); + + let $inputEl = null; + let $dropdownEl = null; + let $wrapper = null; + const tzListSel = '.dropdown-content ul li a.is-active'; + + describe('Initialize', () => { + describe('with dropdown already loaded', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.dropdown'); + $inputEl = $('#schedule_cron_timezone'); + $dropdownEl = $('.js-timezone-dropdown'); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + }); + }); + + it('can take an $inputEl in the constructor', () => { + const tzStr = '[UTC + 5.5] Sri Jayawardenepura'; + const tzValue = 'Asia/Colombo'; + + expect($inputEl.val()).toBe('UTC'); + + $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click'); + + const val = $inputEl.val(); + + expect(val).toBe(tzValue); + expect(val).not.toBe('UTC'); + }); + + it('will format data array of timezones into a list of offsets', () => { + const data = $dropdownEl.data('data'); + const formatted = $wrapper.find(tzListSel).text(); + + data.forEach(item => { + expect(formatted).toContain(formatTimezone(item)); + }); + }); + + it('will default the timezone to UTC', () => { + const tz = $inputEl.val(); + + expect(tz).toBe('UTC'); + }); + }); + + describe('without dropdown loaded', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.dropdown'); + $inputEl = $('#schedule_cron_timezone'); + $dropdownEl = $('.js-timezone-dropdown'); + }); + + it('will populate the list of UTC offsets after the dropdown is loaded', () => { + expect($wrapper.find(tzListSel).length).toEqual(0); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + }); + + expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length); + }); + + it('will call a provided handler when a new timezone is selected', () => { + const onSelectTimezone = jasmine.createSpy('onSelectTimezoneMock'); + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + onSelectTimezone, + }); + + $wrapper + .find(tzListSel) + .first() + .trigger('click'); + + expect(onSelectTimezone).toHaveBeenCalled(); + }); + }); + }); + + describe('formatUtcOffset', () => { + it('will convert negative utc offsets in seconds to hours and minutes', () => { + expect(formatUtcOffset(-21600)).toEqual('- 6'); + }); + + it('will convert positive utc offsets in seconds to hours and minutes', () => { + expect(formatUtcOffset(25200)).toEqual('+ 7'); + expect(formatUtcOffset(49500)).toEqual('+ 13.75'); + }); + + it('will return 0 when given a string', () => { + expect(formatUtcOffset('BLAH')).toEqual('0'); + expect(formatUtcOffset('$%$%')).toEqual('0'); + }); + + it('will return 0 when given an array', () => { + expect(formatUtcOffset(['an', 'array'])).toEqual('0'); + }); + + it('will return 0 when given an object', () => { + expect(formatUtcOffset({ some: '', object: '' })).toEqual('0'); + }); + + it('will return 0 when given null', () => { + expect(formatUtcOffset(null)).toEqual('0'); + }); + + it('will return 0 when given undefined', () => { + expect(formatUtcOffset(undefined)).toEqual('0'); + }); + + it('will return 0 when given empty input', () => { + expect(formatUtcOffset('')).toEqual('0'); + }); + }); + + describe('formatTimezone', () => { + it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => { + expect( + formatTimezone({ + name: 'Chatham Is.', + offset: 49500, + identifier: 'Pacific/Chatham', + }), + ).toEqual('[UTC + 13.75] Chatham Is.'); + }); + + it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => { + expect( + formatTimezone({ + name: 'Saskatchewan', + offset: -21600, + identifier: 'America/Regina', + }), + ).toEqual('[UTC - 6] Saskatchewan'); + }); + + it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => { + expect( + formatTimezone({ + name: 'Accra', + offset: 0, + identifier: 'Africa/Accra', + }), + ).toEqual('[UTC 0] Accra'); + }); + }); +}); diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js index 657e88ecb96..f46ea5a0499 100644 --- a/spec/javascripts/sidebar/todo_spec.js +++ b/spec/javascripts/sidebar/todo_spec.js @@ -116,7 +116,7 @@ describe('SidebarTodo', () => { const dataAttributes = { issuableId: '1', issuableType: 'epic', - originalTitle: 'Mark todo as done', + originalTitle: '', placement: 'left', container: 'body', boundary: 'viewport', @@ -130,6 +130,10 @@ describe('SidebarTodo', () => { }); }); + it('check button label computed property', () => { + expect(vm.buttonLabel).toEqual('Mark todo as done'); + }); + it('renders button label element when `collapsed` prop is `false`', () => { const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner'); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 02c476f2871..cd77b0ab815 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -15,6 +15,16 @@ describe('MRWidgetHeader', () => { gon.relative_url_root = ''; }); + const expectDownloadDropdownItems = () => { + const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches'); + const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff'); + + expect(downloadEmailPatchesEl.textContent.trim()).toEqual('Email patches'); + expect(downloadEmailPatchesEl.getAttribute('href')).toEqual('/mr/email-patches'); + expect(downloadPlainDiffEl.textContent.trim()).toEqual('Plain diff'); + expect(downloadPlainDiffEl.getAttribute('href')).toEqual('/mr/plainDiffPath'); + }; + describe('computed', () => { describe('shouldShowCommitsBehindText', () => { it('return true when there are divergedCommitsCount', () => { @@ -207,21 +217,7 @@ describe('MRWidgetHeader', () => { }); it('renders download dropdown with links', () => { - expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual( - 'Email patches', - ); - - expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual( - '/mr/email-patches', - ); - - expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual( - 'Plain diff', - ); - - expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual( - '/mr/plainDiffPath', - ); + expectDownloadDropdownItems(); }); }); @@ -250,10 +246,8 @@ describe('MRWidgetHeader', () => { expect(button).toEqual(null); }); - it('does not render download dropdown with links', () => { - expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null); - - expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null); + it('renders download dropdown with links', () => { + expectDownloadDropdownItems(); }); }); diff --git a/spec/lib/api/entities/job_request/image_spec.rb b/spec/lib/api/entities/job_request/image_spec.rb new file mode 100644 index 00000000000..092c181ae9c --- /dev/null +++ b/spec/lib/api/entities/job_request/image_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::JobRequest::Image do + let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]} + let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)} + let(:entity) { described_class.new(image) } + + subject { entity.as_json } + + it 'returns the image name' do + expect(subject[:name]).to eq 'image_name' + end + + it 'returns the entrypoint' do + expect(subject[:entrypoint]).to eq ['foo'] + end + + it 'returns the ports' do + expect(subject[:ports]).to eq ports + end + + context 'when the ports param is nil' do + let(:ports) { nil } + + it 'does not return the ports' do + expect(subject[:ports]).to be_nil + end + end +end diff --git a/spec/lib/api/entities/job_request/port_spec.rb b/spec/lib/api/entities/job_request/port_spec.rb new file mode 100644 index 00000000000..40ab4cd6231 --- /dev/null +++ b/spec/lib/api/entities/job_request/port_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::API::Entities::JobRequest::Port do + let(:port) { double(number: 80, protocol: 'http', name: 'name')} + let(:entity) { described_class.new(port) } + + subject { entity.as_json } + + it 'returns the port number' do + expect(subject[:number]).to eq 80 + end + + it 'returns if the port protocol' do + expect(subject[:protocol]).to eq 'http' + end + + it 'returns the port name' do + expect(subject[:name]).to eq 'name' + end +end diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 773a52cdfbc..6e20e0ef5c3 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do it 'populates fabricated object with the proper name attribute' do expect(subject.name).to eq(image_name) end + + it 'does not populate the ports' do + expect(subject.ports).to be_empty + end end context 'when image is defined as hash' do let(:entrypoint) { '/bin/sh' } - let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) } + + let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) } it 'fabricates an object of the proper class' do is_expected.to be_kind_of(described_class) @@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do expect(subject.name).to eq(image_name) expect(subject.entrypoint).to eq(entrypoint) end + + it 'populates the ports' do + port = subject.ports.first + expect(port.number).to eq 80 + expect(port.protocol).to eq 'http' + expect(port.name).to eq 'default_port' + end end context 'when image name is empty' do @@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do expect(subject.first).to be_kind_of(described_class) expect(subject.first.name).to eq(service_image_name) end + + it 'does not populate the ports' do + expect(subject.first.ports).to be_empty + end end context 'when service is defined as hash' do @@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do let(:service_command) { 'sleep 30' } let(:job) do create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, - alias: service_alias, command: service_command }] }) + alias: service_alias, command: service_command, ports: [80] }] }) end it 'fabricates an non-empty array of objects' do @@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.alias).to eq(service_alias) expect(subject.first.command).to eq(service_command) + + port = subject.first.ports.first + expect(port.number).to eq 80 + expect(port.protocol).to eq 'http' + expect(port.name).to eq 'default_port' end end diff --git a/spec/lib/gitlab/ci/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb new file mode 100644 index 00000000000..1413780dfa6 --- /dev/null +++ b/spec/lib/gitlab/ci/build/port_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Port do + subject { described_class.new(port) } + + context 'when port is defined as an integer' do + let(:port) { 80 } + + it 'populates the object' do + expect(subject.number).to eq 80 + expect(subject.protocol).to eq described_class::DEFAULT_PORT_PROTOCOL + expect(subject.name).to eq described_class::DEFAULT_PORT_NAME + end + end + + context 'when port is defined as hash' do + let(:port) { { number: 80, protocol: 'https', name: 'port_name' } } + + it 'populates the object' do + expect(subject.number).to eq 80 + expect(subject.protocol).to eq 'https' + expect(subject.name).to eq 'port_name' + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 1a4d9ed5517..1ebdda398b9 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do expect(entry.entrypoint).to be_nil end end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to be_nil + end + end end context 'when configuration is a hash' do @@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do expect(entry.entrypoint).to eq %w(/bin/sh run) end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } } + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("image config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to eq ports + end + end + end + end end context 'when entry value is not correct' do @@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'image config should be a hash or a string' + expect(entry.errors.first) + .to match /config should be a hash or a string/ end end @@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'image config contains unknown keys: non_existing' + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ end end diff --git a/spec/lib/gitlab/ci/config/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb new file mode 100644 index 00000000000..5f8f294334e --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Port do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is a string' do + let(:config) { 80 } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq(number: 80) + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "is nil" do + expect(entry.protocol).to be_nil + end + end + + describe '#name' do + it "is nil" do + expect(entry.name).to be_nil + end + end + end + + context 'when configuration is a hash' do + context 'with the complete hash' do + let(:config) do + { number: 80, + protocol: 'http', + name: 'foobar' } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq config + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "returns port protocol" do + expect(entry.protocol).to eq 'http' + end + end + + describe '#name' do + it "returns port name" do + expect(entry.name).to eq 'foobar' + end + end + end + + context 'with only the port number' do + let(:config) { { number: 80 } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq(number: 80) + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "is nil" do + expect(entry.protocol).to be_nil + end + end + + describe '#name' do + it "is nil" do + expect(entry.name).to be_nil + end + end + end + + context 'without the number' do + let(:config) { { protocol: 'http' } } + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + end + + context 'when configuration is invalid' do + let(:config) { '80' } + + describe '#valid?' do + it 'is valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when protocol' do + let(:config) { { number: 80, protocol: protocol, name: 'foobar' } } + + context 'is http' do + let(:protocol) { 'http' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'is https' do + let(:protocol) { 'https' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'is neither http nor https' do + let(:protocol) { 'foo' } + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors).to include("port protocol should be http or https") + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/ports_spec.rb b/spec/lib/gitlab/ci/config/entry/ports_spec.rb new file mode 100644 index 00000000000..2063bd1d86c --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/ports_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Ports do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is valid' do + let(:config) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq(config) + end + end + end + + context 'when configuration is invalid' do + let(:config) { 'postgresql:9.5' } + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + + context 'when any of the ports' do + before do + expect(entry).not_to be_valid + expect(entry.errors.count).to eq 1 + end + + context 'have the same name' do + let(:config) do + [{ number: 80, protocol: 'http', name: 'foobar' }, + { number: 81, protocol: 'http', name: 'foobar' }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors.first).to match /each port name must be different/ + end + end + end + + context 'have the same port' do + let(:config) do + [{ number: 80, protocol: 'http', name: 'foobar' }, + { number: 80, protocol: 'http', name: 'foobar1' }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors.first).to match /each port number can only be referenced once/ + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 9ebf947a751..d5bd139b5f1 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do expect(entry.command).to be_nil end end + + describe '#ports' do + it "returns service's ports" do + expect(entry.ports).to be_nil + end + end end context 'when configuration is a hash' do @@ -81,6 +87,40 @@ describe Gitlab::Ci::Config::Entry::Service do expect(entry.entrypoint).to eq %w(/bin/sh run) end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } + end + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to eq ports + end + end + end + end end context 'when entry value is not correct' do @@ -88,8 +128,8 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'service config should be a hash or a string' + expect(entry.errors.first) + .to match /config should be a hash or a string/ end end @@ -105,8 +145,8 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'service config contains unknown keys: non_existing' + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ end end @@ -116,4 +156,26 @@ describe Gitlab::Ci::Config::Entry::Service do end end end + + context 'when service has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) do + { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } + end + + it 'alias field is mandatory' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service alias can't be blank") + end + end + + context 'when service does not have ports' do + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) } + end + + it 'alias field is optional' do + expect(entry).to be_valid + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index 7c4319aee63..d5a1316f665 100644 --- a/spec/lib/gitlab/ci/config/entry/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do end end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] } + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }]) + end + end + + describe 'services alias' do + context 'when they are not unique' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'postgres_old', ports: [81] }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("services config alias must be unique in services with ports") + end + end + end + + context 'when they are unique' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'ruby', ports: [81] }] + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when one of the duplicated alias is in a service without ports' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'postgres_old' }] + end + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when there are not any ports' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old' }, + { name: 'ruby', alias: 'postgres_old' }] + end + + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 18f255c1ab7..00b2753c5fc 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -123,6 +123,63 @@ describe Gitlab::Ci::Config do ) end end + + context 'when ports have been set' do + context 'in the main image' do + let(:yml) do + <<-EOS + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("image config contains disallowed keys: ports") + end + end + + context 'in the job image' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("jobs:test:image config contains disallowed keys: ports") + end + end + + context 'in the services' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: ruby:2.2 + services: + - name: test + alias: test + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("jobs:test:services:service config contains disallowed keys: ports") + end + end + end end context "when using 'include' directive" do diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index fbbd58280a9..4e3681cd943 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -4,6 +4,9 @@ require 'spec_helper' describe "CI YML Templates" do ABSTRACT_TEMPLATES = %w[Serverless].freeze + # These templates depend on the presence of the `project` + # param to enable processing of `include:` within CI config. + PROJECT_DEPENDENT_TEMPLATES = %w[Auto-DevOps DAST].freeze def self.concrete_templates Gitlab::Template::GitlabCiYmlTemplate.all.reject do |template| @@ -20,7 +23,10 @@ describe "CI YML Templates" do describe 'concrete templates with CI/CD jobs' do concrete_templates.each do |template| it "#{template.name} template should be valid" do - expect { Gitlab::Ci::YamlProcessor.new(template.content) } + # Trigger processing of included files + project = create(:project, :test_repo) if PROJECT_DEPENDENT_TEMPLATES.include?(template.name) + + expect { Gitlab::Ci::YamlProcessor.new(template.content, project: project) } .not_to raise_error end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 29638ef47c5..63a0d54dcfc 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1233,7 +1233,7 @@ module Gitlab config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string") end it "returns errors if job services parameter is not an array" do @@ -1247,7 +1247,7 @@ module Gitlab config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string") end it "returns error if job configuration is invalid" do diff --git a/spec/lib/gitlab/graphql/tracing_spec.rb b/spec/lib/gitlab/graphql/tracing_spec.rb deleted file mode 100644 index 6bae737d0f6..00000000000 --- a/spec/lib/gitlab/graphql/tracing_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::Graphql::Tracing do - let!(:graphql_duration_seconds) { double('Gitlab::Metrics::NullMetric') } - - before do - allow(Gitlab::Metrics) - .to receive(:histogram) - .with(:graphql_duration_seconds, 'GraphQL execution time') - .and_return(graphql_duration_seconds) - end - - it 'updates graphql histogram with expected labels' do - query = 'query { users { id } }' - - expect_metric('graphql.lex', 'lex') - expect_metric('graphql.parse', 'parse') - expect_metric('graphql.validate', 'validate') - expect_metric('graphql.analyze', 'analyze_multiplex') - expect_metric('graphql.execute', 'execute_query_lazy') - expect_metric('graphql.execute', 'execute_multiplex') - - GitlabSchema.execute(query) - end - - private - - def expect_metric(platform_key, key) - expect(graphql_duration_seconds) - .to receive(:observe) - .with({ platform_key: platform_key, key: key }, be > 0.0) - end -end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb index 5a88b23aa82..a6589f0c0a3 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb @@ -9,9 +9,35 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery do let(:query_params) { [environment.id] } it 'queries using specific time' do - expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) - + expect(client).to receive(:query_range) + .with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) expect(query_result).not_to be_nil end + + context 'when start and end time parameters are provided' do + let(:query_params) { [environment.id, start_time, end_time] } + + context 'as unix timestamps' do + let(:start_time) { 4.hours.ago.to_f } + let(:end_time) { 2.hours.ago.to_f } + + it 'queries using the provided times' do + expect(client).to receive(:query_range) + .with(anything, start: start_time, stop: end_time) + expect(query_result).not_to be_nil + end + end + + context 'as Date/Time objects' do + let(:start_time) { 4.hours.ago } + let(:end_time) { 2.hours.ago } + + it 'queries using the provided times converted to unix' do + expect(client).to receive(:query_range) + .with(anything, start: start_time.to_f, stop: end_time.to_f) + expect(query_result).not_to be_nil + end + end + end end end diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb index 7148261b1e4..25a2d290f76 100644 --- a/spec/models/concerns/prometheus_adapter_spec.rb +++ b/spec/models/concerns/prometheus_adapter_spec.rb @@ -6,14 +6,15 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do include PrometheusHelpers include ReactiveCachingHelpers - class TestClass - include PrometheusAdapter - end - let(:project) { create(:prometheus_project) } let(:service) { project.prometheus_service } - let(:described_class) { TestClass } + let(:described_class) do + Class.new do + include PrometheusAdapter + end + end + let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery } describe '#query' do @@ -76,6 +77,28 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do end end end + + describe 'additional_metrics' do + let(:additional_metrics_environment_query) { Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery } + let(:environment) { build_stubbed(:environment, slug: 'env-slug') } + let(:time_window) { [1552642245.067, 1552642095.831] } + + around do |example| + Timecop.freeze { example.run } + end + + context 'with valid data' do + subject { service.query(:additional_metrics_environment, environment, *time_window) } + + before do + stub_reactive_cache(service, prometheus_data, additional_metrics_environment_query, environment.id, *time_window) + end + + it 'returns reactive data' do + expect(subject).to eq(prometheus_data) + end + end + end end describe '#calculate_reactive_cache' do @@ -120,4 +143,24 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do end end end + + describe '#build_query_args' do + subject { service.build_query_args(*args) } + + context 'when active record models are included' do + let(:args) { [double(:environment, id: 12)] } + + it 'serializes by id' do + is_expected.to eq [12] + end + end + + context 'when args are safe for serialization' do + let(:args) { ['stringy arg', 5, 6.0, :symbolic_arg] } + + it 'does nothing' do + is_expected.to eq args + end + end + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 448ed35cb1e..cfe7c7ef0b0 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -592,7 +592,9 @@ describe Environment do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do it 'returns the terminals from the deployment service' do - expect(project.deployment_platform) + deployment_platform_target = Gitlab.ee? ? environment : project + + expect(deployment_platform_target.deployment_platform) .to receive(:terminals).with(environment) .and_return(:fake_terminals) @@ -685,7 +687,8 @@ describe Environment do describe '#additional_metrics' do let(:project) { create(:prometheus_project) } - subject { environment.additional_metrics } + let(:metric_params) { [] } + subject { environment.additional_metrics(*metric_params) } context 'when the environment has additional metrics' do before do @@ -693,12 +696,26 @@ describe Environment do end it 'returns the additional metrics from the deployment service' do - expect(environment.prometheus_adapter).to receive(:query) - .with(:additional_metrics_environment, environment) - .and_return(:fake_metrics) + expect(environment.prometheus_adapter) + .to receive(:query) + .with(:additional_metrics_environment, environment) + .and_return(:fake_metrics) is_expected.to eq(:fake_metrics) end + + context 'when time window arguments are provided' do + let(:metric_params) { [1552642245.067, Time.now] } + + it 'queries with the expected parameters' do + expect(environment.prometheus_adapter) + .to receive(:query) + .with(:additional_metrics_environment, environment, *metric_params.map(&:to_f)) + .and_return(:fake_metrics) + + is_expected.to eq(:fake_metrics) + end + end end context 'when the environment does not have metrics' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 892fdc4e4e9..6f34ef9c1bc 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -805,6 +805,14 @@ describe MergeRequest do expect(merge_request.commits).not_to be_empty expect(merge_request.related_notes.count).to eq(3) end + + it "excludes system notes for commits" do + system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id, + project: merge_request.project) + + expect(merge_request.related_notes.count).to eq(2) + expect(merge_request.related_notes).not_to include(system_note) + end end describe '#for_fork?' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2158d3cf3e6..33e514cd7b9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2143,6 +2143,15 @@ describe Project do expect(project.add_import_job).to eq(import_jid) end + + context 'without repository' do + it 'schedules RepositoryImportWorker' do + project = create(:project, import_url: generate(:url)) + + expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid) + expect(project.add_import_job).to eq(import_jid) + end + end end context 'not forked' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index a2d4fad9292..77c88a04cde 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -195,6 +195,30 @@ describe ProjectTeam do end end + describe '#add_users' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:project) { create(:project) } + + it 'add the given users to the team' do + project.team.add_users([user1, user2], :reporter) + + expect(project.team.reporter?(user1)).to be(true) + expect(project.team.reporter?(user2)).to be(true) + end + end + + describe '#add_user' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + it 'add the given user to the team' do + project.team.add_user(user, :reporter) + + expect(project.team.reporter?(user)).to be(true) + end + end + describe "#human_max_access" do it 'returns Maintainer role' do user = create(:user) diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 7ea67f31534..2525a6aebe0 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -1,4 +1,3 @@ -# coding: utf-8 # frozen_string_literal: true require "spec_helper" diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4259fda7f04..1d139200535 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -729,6 +729,14 @@ describe API::MergeRequests do end describe "GET /projects/:id/merge_requests/:merge_request_iid" do + it 'matches json schema' do + merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/merge_request') + end + it 'exposes known attributes' do create(:award_emoji, :downvote, awardable: merge_request) create(:award_emoji, :upvote, awardable: merge_request) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 52599db9a9e..c26d31c5e0d 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -399,6 +399,13 @@ describe API::Pipelines do describe 'GET /projects/:id/pipelines/:pipeline_id' do context 'authorized user' do + it 'exposes known attributes' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pipeline/detail') + end + it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 3ccedd8dd06..5fdc7c64030 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -470,11 +470,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(json_response['token']).to eq(job.token) expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh', 'ports' => [] }) expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, - 'alias' => nil, 'command' => nil }, + 'alias' => nil, 'command' => nil, 'ports' => [] }, { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', - 'alias' => 'docker', 'command' => 'sleep 30' }]) + 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to eq(expected_cache) @@ -853,6 +853,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + describe 'port support' do + let(:job) { create(:ci_build, pipeline: pipeline, options: options) } + + context 'when job image has ports' do + let(:options) do + { + image: { + name: 'ruby', + ports: [80] + }, + services: ['mysql'] + } + end + + it 'returns the image ports' do + request_job + + expect(response).to have_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]), + 'services' => all(a_hash_including('name' => 'mysql'))) + end + end + + context 'when job services settings has ports' do + let(:options) do + { + image: 'ruby', + services: [ + { + name: 'tomcat', + ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }] + } + ] + } + end + + it 'returns the service ports' do + request_job + + expect(response).to have_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => a_hash_including('name' => 'ruby'), + 'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }]))) + end + end + end + def request_job(token = runner.token, **params) new_params = params.merge(token: token, last_update: last_update) post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 24707cd2d41..866d709d446 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -306,6 +306,56 @@ describe Ci::CreatePipelineService do it_behaves_like 'a failed pipeline' end + + context 'when config has ports' do + context 'in the main image' do + let(:ci_yaml) do + <<-EOS + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it_behaves_like 'a failed pipeline' + end + + context 'in the job image' do + let(:ci_yaml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it_behaves_like 'a failed pipeline' + end + + context 'in the service' do + let(:ci_yaml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: ruby:2.2 + services: + - name: test + ports: + - 80 + EOS + end + + it_behaves_like 'a failed pipeline' + end + end end context 'when commit contains a [ci skip] directive' do diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index e151db5827f..2d960fc9f08 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -31,6 +31,20 @@ describe Git::TagPushService do end end + describe 'System Hooks' do + let!(:push_data) { service.tap(&:execute).push_data } + + it "executes system hooks after pushing a tag" do + expect_next_instance_of(SystemHooksService) do |system_hooks_service| + expect(system_hooks_service) + .to receive(:execute_hooks) + .with(push_data, :tag_push_hooks) + end + + service.execute + end + end + describe "Pipelines" do subject { service.execute } diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index d80d0f5a8a8..98114ea4919 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -82,44 +82,6 @@ describe Groups::DestroyService do expect(Group.unscoped.count).to eq(2) end end - - context 'potential race conditions' do - context "when the `GroupDestroyWorker` task runs immediately" do - it "deletes the group" do - # Commit the contents of this spec's transaction so far - # so subsequent db connections can see it. - # - # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No - # transaction is currently in progress". Without this, this - # spec will always be green, since the group created in setup - # cannot be seen by any other connections / threads in this spec. - Group.connection.commit_db_transaction - - group_record = run_with_new_database_connection do |conn| - conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first - end - - expect(group_record).not_to be_nil - - # Execute the contents of `GroupDestroyWorker` in a separate thread, to - # simulate data manipulation by the Sidekiq worker (different database - # connection / transaction). - expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id| - Thread.new { m[group_id, user_id] }.join(5) - end - - # Kick off the initial group destroy in a new thread, so that - # it doesn't share this spec's database transaction. - Thread.new { described_class.new(group, user).async_execute }.join(5) - - group_record = run_with_new_database_connection do |conn| - conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first - end - - expect(group_record).to be_nil - end - end - end end describe 'synchronous delete' do diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index af4daff336b..96fff20f7fb 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -128,37 +128,19 @@ describe Notes::BuildService do subject { described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute } - shared_examples 'an individual note reply' do - it 'builds another individual note' do - expect(subject).to be_valid - expect(subject).to be_a(Note) - expect(subject.discussion_id).not_to eq(note.discussion_id) - end + it 'sets the note up to be in reply to that note' do + expect(subject).to be_valid + expect(subject).to be_a(DiscussionNote) + expect(subject.discussion_id).to eq(note.discussion_id) end - context 'when reply_to_individual_notes is disabled' do - before do - stub_feature_flags(reply_to_individual_notes: false) - end - - it_behaves_like 'an individual note reply' - end + context 'when noteable does not support replies' do + let(:note) { create(:note_on_commit) } - context 'when reply_to_individual_notes is enabled' do - before do - stub_feature_flags(reply_to_individual_notes: true) - end - - it 'sets the note up to be in reply to that note' do + it 'builds another individual note' do expect(subject).to be_valid - expect(subject).to be_a(DiscussionNote) - expect(subject.discussion_id).to eq(note.discussion_id) - end - - context 'when noteable does not support replies' do - let(:note) { create(:note_on_commit) } - - it_behaves_like 'an individual note reply' + expect(subject).to be_a(Note) + expect(subject.discussion_id).not_to eq(note.discussion_id) end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 8d8e81173ff..bcbb8950910 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -298,41 +298,20 @@ describe Notes::CreateService do subject { described_class.new(project, user, reply_opts).execute } - context 'when reply_to_individual_notes is disabled' do - before do - stub_feature_flags(reply_to_individual_notes: false) - end - - it 'creates an individual note' do - expect(subject.type).to eq(nil) - expect(subject.discussion_id).not_to eq(existing_note.discussion_id) - end - - it 'does not convert existing note' do - expect { subject }.not_to change { existing_note.reload.type } - end + it 'creates a DiscussionNote in reply to existing note' do + expect(subject).to be_a(DiscussionNote) + expect(subject.discussion_id).to eq(existing_note.discussion_id) end - context 'when reply_to_individual_notes is enabled' do - before do - stub_feature_flags(reply_to_individual_notes: true) - end - - it 'creates a DiscussionNote in reply to existing note' do - expect(subject).to be_a(DiscussionNote) - expect(subject.discussion_id).to eq(existing_note.discussion_id) - end - - it 'converts existing note to DiscussionNote' do - expect do - existing_note + it 'converts existing note to DiscussionNote' do + expect do + existing_note - Timecop.freeze(Time.now + 1.minute) { subject } + Timecop.freeze(Time.now + 1.minute) { subject } - existing_note.reload - end.to change { existing_note.type }.from(nil).to('DiscussionNote') - .and change { existing_note.updated_at } - end + existing_note.reload + end.to change { existing_note.type }.from(nil).to('DiscussionNote') + .and change { existing_note.updated_at } end end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 8b0f9c8ade2..c7e5cca324f 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -10,6 +10,7 @@ describe QuickActions::InterpretService do let(:milestone) { create(:milestone, project: project, title: '9.10') } let(:commit) { create(:commit, project: project) } let(:inprogress) { create(:label, project: project, title: 'In Progress') } + let(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') } let(:bug) { create(:label, project: project, title: 'Bug') } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } let(:service) { described_class.new(project, developer) } @@ -94,6 +95,26 @@ describe QuickActions::InterpretService do end end + shared_examples 'multiword label name starting without ~' do + it 'fetches label ids and populates add_label_ids if content contains /label' do + helmchart # populate the label + _, updates = service.execute(content, issuable) + + expect(updates).to eq(add_label_ids: [helmchart.id]) + end + end + + shared_examples 'label name is included in the middle of another label name' do + it 'ignores the sublabel when the content contains the includer label name' do + helmchart # populate the label + create(:label, project: project, title: 'Chart') + + _, updates = service.execute(content, issuable) + + expect(updates).to eq(add_label_ids: [helmchart.id]) + end + end + shared_examples 'unlabel command' do it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do issuable.update!(label_ids: [inprogress.id]) # populate the label @@ -624,6 +645,26 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + it_behaves_like 'multiword label name starting without ~' do + let(:content) { %(/label "#{helmchart.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'multiword label name starting without ~' do + let(:content) { %(/label "#{helmchart.title}") } + let(:issuable) { merge_request } + end + + it_behaves_like 'label name is included in the middle of another label name' do + let(:content) { %(/label ~"#{helmchart.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'label name is included in the middle of another label name' do + let(:content) { %(/label ~"#{helmchart.title}") } + let(:issuable) { merge_request } + end + it_behaves_like 'unlabel command' do let(:content) { %(/unlabel ~"#{inprogress.title}") } let(:issuable) { issue } diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index 73156d18c1b..693b796fbdc 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -23,10 +23,13 @@ shared_examples 'variable list' do end end - it 'adds empty variable' do + it 'adds a new protected variable' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('') + find('.js-ci-variable-input-value').set('key_value') + find('.ci-variable-protected-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') end click_button('Save variables') @@ -37,17 +40,17 @@ shared_examples 'variable list' do # We check the first row because it re-sorts to alphabetical order on refresh page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') end end - it 'adds new protected variable' do + it 'defaults to masked' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') find('.js-ci-variable-input-value').set('key_value') - find('.ci-variable-protected-item .js-project-feature-toggle').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') end click_button('Save variables') @@ -59,7 +62,7 @@ shared_examples 'variable list' do page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do expect(find('.js-ci-variable-input-key').value).to eq('key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') end end @@ -163,27 +166,6 @@ shared_examples 'variable list' do end end - it 'edits variable with empty value' do - page.within('.js-ci-variable-list-section') do - click_button('Reveal value') - - page.within('.js-row:nth-child(1)') do - find('.js-ci-variable-input-key').set('new_key') - find('.js-ci-variable-input-value').set('') - end - - click_button('Save variables') - wait_for_requests - - visit page_path - - page.within('.js-row:nth-child(1)') do - expect(find('.js-ci-variable-input-key').value).to eq('new_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('') - end - end - end - it 'edits variable to be protected' do # Create the unprotected variable page.within('.js-ci-variable-list-section .js-row:last-child') do @@ -251,6 +233,57 @@ shared_examples 'variable list' do end end + it 'edits variable to be unmasked' do + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + + find('.ci-variable-masked-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + end + end + + it 'edits variable to be masked' do + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + + find('.ci-variable-masked-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + + find('.ci-variable-masked-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + end + end + it 'handles multiple edits and deletion in the middle' do page.within('.js-ci-variable-list-section') do # Create 2 variables @@ -297,11 +330,11 @@ shared_examples 'variable list' do it 'shows validation error box about duplicate keys' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value1') + find('.js-ci-variable-input-value').set('value123') end page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value2') + find('.js-ci-variable-input-value').set('value456') end click_button('Save variables') @@ -314,4 +347,34 @@ shared_examples 'variable list' do expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/) end end + + it 'shows validation error box about empty values' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('empty_value') + find('.js-ci-variable-input-value').set('') + end + + click_button('Save variables') + wait_for_requests + + page.within('.js-ci-variable-list-section') do + expect(all('.js-ci-variable-error-box ul li').count).to eq(1) + expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) + end + end + + it 'shows validation error box about unmaskable values' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('unmaskable_value') + find('.js-ci-variable-input-value').set('???') + end + + click_button('Save variables') + wait_for_requests + + page.within('.js-ci-variable-list-section') do + expect(all('.js-ci-variable-error-box ul li').count).to eq(1) + expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) + end + end end diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb index 8bcd26ec0cd..3ad6e067674 100644 --- a/spec/support/shared_context/policies/project_policy_shared_context.rb +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do read_project_for_iids read_issue_iid read_label read_milestone read_project_snippet read_project_member read_note create_project create_issue create_note upload_file create_merge_request_in - award_emoji read_release + award_emoji ] end @@ -24,7 +24,7 @@ RSpec.shared_context 'ProjectPolicy context' do download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue + read_merge_request download_wiki_code read_sentry_issue read_release ] end diff --git a/spec/support/shared_examples/time_tracking_shared_examples.rb b/spec/support/shared_examples/time_tracking_shared_examples.rb deleted file mode 100644 index 909d4e2ee8d..00000000000 --- a/spec/support/shared_examples/time_tracking_shared_examples.rb +++ /dev/null @@ -1,85 +0,0 @@ -shared_examples 'issuable time tracker' do - it 'renders the sidebar component empty state' do - page.within '.time-tracking-no-tracking-pane' do - expect(page).to have_content 'No estimate or time spent' - end - end - - it 'updates the sidebar component when estimate is added' do - submit_time('/estimate 3w 1d 1h') - - wait_for_requests - page.within '.time-tracking-estimate-only-pane' do - expect(page).to have_content '3w 1d 1h' - end - end - - it 'updates the sidebar component when spent is added' do - submit_time('/spend 3w 1d 1h') - - wait_for_requests - page.within '.time-tracking-spend-only-pane' do - expect(page).to have_content '3w 1d 1h' - end - end - - it 'shows the comparison when estimate and spent are added' do - submit_time('/estimate 3w 1d 1h') - submit_time('/spend 3w 1d 1h') - - wait_for_requests - page.within '.time-tracking-comparison-pane' do - expect(page).to have_content '3w 1d 1h' - end - end - - it 'updates the sidebar component when estimate is removed' do - submit_time('/estimate 3w 1d 1h') - submit_time('/remove_estimate') - - page.within '.time-tracking-component-wrap' do - expect(page).to have_content 'No estimate or time spent' - end - end - - it 'updates the sidebar component when spent is removed' do - submit_time('/spend 3w 1d 1h') - submit_time('/remove_time_spent') - - page.within '.time-tracking-component-wrap' do - expect(page).to have_content 'No estimate or time spent' - end - end - - it 'shows the help state when icon is clicked' do - page.within '.time-tracking-component-wrap' do - find('.help-button').click - expect(page).to have_content 'Track time with quick actions' - expect(page).to have_content 'Learn more' - end - end - - it 'hides the help state when close icon is clicked' do - page.within '.time-tracking-component-wrap' do - find('.help-button').click - find('.close-help-button').click - - expect(page).not_to have_content 'Track time with quick actions' - expect(page).not_to have_content 'Learn more' - end - end - - it 'displays the correct help url' do - page.within '.time-tracking-component-wrap' do - find('.help-button').click - - expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') - end - end -end - -def submit_time(quick_action) - fill_in 'note[note]', with: quick_action - find('.js-comment-submit-button').click - wait_for_requests -end diff --git a/yarn.lock b/yarn.lock index 5d40bec659d..4de579fb290 100644 --- a/yarn.lock +++ b/yarn.lock @@ -658,10 +658,10 @@ eslint-plugin-promise "^4.0.1" eslint-plugin-vue "^5.0.0" -"@gitlab/svgs@^1.57.0": - version "1.57.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.57.0.tgz#969ac7bf16337d5de3808fee6fb5c13eefd99478" - integrity sha512-AAVvPDaxCsojmOyVVTyaOcob+bPhtYJ+GbtmmNNCHg2dXYDAEgy3+TYzAfV5fQ08TCZ9DPiKEjDIi2ODh0x/8g== +"@gitlab/svgs@^1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f" + integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg== "@gitlab/ui@^3.0.0": version "3.0.0" @@ -1164,15 +1164,7 @@ apollo-link-http-common@^0.2.8: ts-invariant "^0.3.2" tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.3.tgz#9bd8d5fe1d88d31dc91dae9ecc22474d451fb70d" - integrity sha512-iL9yS2OfxYhigme5bpTbmRyC+Htt6tyo2fRMHT3K1XRL/C5IQDDz37OjpPy4ndx7WInSvfSZaaOTKFja9VWqSw== - dependencies: - apollo-utilities "^1.0.0" - zen-observable-ts "^0.8.10" - -apollo-link@^1.2.11, apollo-link@^1.2.6: +apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3, apollo-link@^1.2.6: version "1.2.11" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d" integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA== @@ -1191,7 +1183,7 @@ apollo-upload-client@^10.0.0: apollo-link-http-common "^0.2.8" extract-files "^5.0.0" -apollo-utilities@1.2.1, apollo-utilities@^1.0.0, apollo-utilities@^1.2.1: +apollo-utilities@1.2.1, apollo-utilities@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c" integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg== @@ -2804,7 +2796,7 @@ d3-dsv@1, d3-dsv@1.0.8: iconv-lite "0.4" rw "1" -d3-ease@1, d3-ease@1.0.3: +d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4= @@ -2920,7 +2912,7 @@ d3-timer@1, d3-timer@1.0.7: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531" integrity sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA== -d3-transition@1, d3-transition@1.1.1: +d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039" integrity sha512-xeg8oggyQ+y5eb4J13iDgKIjUcEfIOZs2BqV/eEmXm2twx80wTzJ4tB4vaZ5BKfz7XsI/DFmQL5me6O27/5ykQ== @@ -11536,13 +11528,6 @@ yeast@0.1.2: resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= -zen-observable-ts@^0.8.10: - version "0.8.10" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz#18e2ce1c89fe026e9621fd83cc05168228fce829" - integrity sha512-5vqMtRggU/2GhePC9OU4sYEWOdvmayp2k3gjPf4F0mXwB3CSbbNznfDUvDJx9O2ZTa1EIXdJhPchQveFKwNXPQ== - dependencies: - zen-observable "^0.8.0" - zen-observable-ts@^0.8.18: version "0.8.18" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8" |