diff options
97 files changed, 1845 insertions, 583 deletions
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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-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/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. + +  + +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'. + +  + +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. + + + +## 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/yaml/README.md b/doc/ci/yaml/README.md index 83a226d3577..e75f7050a09 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -501,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: @@ -518,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/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 69dfad829b4..df413a11af0 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -162,8 +162,7 @@ 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) @@ -208,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/lib/api/entities.rb b/lib/api/entities.rb index 4533305bfd3..cc62b5a3661 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1389,8 +1389,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/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/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/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 bb145a0984f..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 "" @@ -8350,9 +8359,6 @@ msgstr "" msgid "Timeago|%s months remaining" msgstr "" -msgid "Timeago|%s seconds ago" -msgstr "" - msgid "Timeago|%s seconds remaining" msgstr "" @@ -8945,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 934a30df261..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", 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/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/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/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/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 e20a621168c..4e3681cd943 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -4,7 +4,9 @@ require 'spec_helper' describe "CI YML Templates" do ABSTRACT_TEMPLATES = %w[Serverless].freeze - PROJECT_DEPENDENT_TEMPLATES = %w[Auto-DevOps].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| 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/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/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/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/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_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 |