diff options
72 files changed, 1397 insertions, 350 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a02b6594fad..4cedfa60b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.5.4 (2017-09-06) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [SECURITY] Prevent a persistent XSS in the commit author block. +- Fix XSS issue in go-get handling. +- Resolve CSRF token leakage via pathname manipulation on environments page. +- Fixes race condition in project uploads. +- Disallow arbitrary properties in `th` and `td` `style` attributes. +- Disallow the `name` attribute on all user-provided markup. + ## 9.5.3 (2017-09-03) - [SECURITY] Filter additional secrets from Rails logs. @@ -203,6 +213,18 @@ entry. - Use a specialized class for querying events to improve performance. - Update build badges to be pipeline badges and display passing instead of success. +## 9.4.6 (2017-09-06) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [SECURITY] Prevent a persistent XSS in the commit author block. +- Fix XSS issue in go-get handling. +- Remove hidden symlinks from project import files. +- Fixes race condition in project uploads. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. +- Disallow arbitrary properties in `th` and `td` `style` attributes. +- Resolve CSRF token leakage via pathname manipulation on environments page. +- Disallow the `name` attribute on all user-provided markup. + ## 9.4.5 (2017-08-14) - Fix deletion of deploy keys linked to other projects. !13162 @@ -453,6 +475,24 @@ entry. - Log rescued exceptions to Sentry. - Remove remaining N+1 queries in merge requests API with emojis and labels. +## 9.3.11 (2017-09-06) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [SECURITY] Prevent a persistent XSS in the commit author block. +- Improve support for external issue references. !12485 +- Use uploads/system directory for personal snippets. +- Remove uploads/appearance symlink. A leftover from a previous migration. +- Fix XSS issue in go-get handling. +- Remove hidden symlinks from project import files. +- Fix an infinite loop when handling user-supplied regular expressions. +- Fixes race condition in project uploads. +- Fixes race condition in project uploads. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. +- Disallow arbitrary properties in `th` and `td` `style` attributes. +- Resolve CSRF token leakage via pathname manipulation on environments page. +- Disallow the `name` attribute on all user-provided markup. +- Renders 404 if given project is not readable by the user on Todos dashboard. + ## 9.3.10 (2017-08-09) - Remove hidden symlinks from project import files. diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 8f0916f768f..4b9fcbec101 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index 91ed8c8467f..f54d573db6e 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -111,11 +111,11 @@ export default { }, methods: { - toggleFolder(folder, folderUrl) { + toggleFolder(folder) { this.store.toggleFolder(folder); if (!folder.isOpen) { - this.fetchChildEnvironments(folder, folderUrl, true); + this.fetchChildEnvironments(folder, true); } }, @@ -143,10 +143,10 @@ export default { .catch(this.errorCallback); }, - fetchChildEnvironments(folder, folderUrl, showLoader = false) { + fetchChildEnvironments(folder, showLoader = false) { this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); - this.service.getFolderContent(folderUrl) + this.service.getFolderContent(folder.folder_path) .then(resp => resp.json()) .then(response => this.store.setfolderContent(folder, response.environments)) .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) @@ -173,12 +173,7 @@ export default { // We need to verify if any folder is open to also update it const openFolders = this.store.getOpenFolders(); if (openFolders.length) { - openFolders.forEach((folder) => { - // TODO - Move this to the backend - const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`; - - return this.fetchChildEnvironments(folder, folderUrl); - }); + openFolders.forEach(folder => this.fetchChildEnvironments(folder)); } }, diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index d8b1b2f1b92..6de01fa53d0 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -410,20 +410,11 @@ export default { this.hasStopAction || this.canRetry; }, - - /** - * Constructs folder URL based on the current location and the folder id. - * - * @return {String} - */ - folderUrl() { - return `${window.location.pathname}/folders/${this.model.folderName}`; - }, }, methods: { onClickFolder() { - eventHub.$emit('toggleFolder', this.model, this.folderUrl); + eventHub.$emit('toggleFolder', this.model); }, }, }; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 57394097944..917a45eb06b 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -13,7 +13,7 @@ export function formatRelevantDigits(number) { let relevantDigits = 0; let formattedNumber = ''; if (!isNaN(Number(number))) { - digitsLeft = number.split('.')[0]; + digitsLeft = number.toString().split('.')[0]; switch (digitsLeft.length) { case 1: relevantDigits = 3; diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index cde2ff7ca2a..6b3e341f936 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -3,7 +3,7 @@ import GraphLegend from './graph/legend.vue'; import GraphFlag from './graph/flag.vue'; import GraphDeployment from './graph/deployment.vue'; - import monitoringPaths from './monitoring_paths.vue'; + import GraphPath from './graph_path.vue'; import MonitoringMixin from '../mixins/monitoring_mixins'; import eventHub from '../event_hub'; import measurements from '../utils/measurements'; @@ -40,8 +40,6 @@ graphHeightOffset: 120, margin: {}, unitOfDisplay: '', - areaColorRgb: '#8fbce8', - lineColorRgb: '#1f78d1', yAxisLabel: '', legendTitle: '', reducedDeploymentData: [], @@ -63,7 +61,7 @@ GraphLegend, GraphFlag, GraphDeployment, - monitoringPaths, + GraphPath, }, computed: { @@ -143,7 +141,7 @@ }, renderAxesPaths() { - this.timeSeries = createTimeSeries(this.graphData.queries[0].result, + this.timeSeries = createTimeSeries(this.graphData.queries[0], this.graphWidth, this.graphHeight, this.graphHeightOffset); @@ -162,7 +160,7 @@ const xAxis = d3.svg.axis() .scale(axisXScale) - .ticks(measurements.xTicks) + .ticks(d3.time.minute, 60) .tickFormat(timeScaleFormat) .orient('bottom'); @@ -238,7 +236,7 @@ class="graph-data" :viewBox="innerViewBox" ref="graphData"> - <monitoring-paths + <graph-path v-for="(path, index) in timeSeries" :key="index" :generated-line-path="path.linePath" @@ -246,7 +244,7 @@ :line-color="path.lineColor" :area-color="path.areaColor" /> - <monitoring-deployment + <graph-deployment :show-deploy-info="showDeployInfo" :deployment-data="reducedDeploymentData" :graph-height="graphHeight" diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index a43dad8e601..dbc48c63747 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -81,6 +81,13 @@ formatMetricUsage(series) { return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; }, + + createSeriesString(index, series) { + if (series.metricTag) { + return `${series.metricTag} ${this.formatMetricUsage(series)}`; + } + return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; + }, }, mounted() { this.$nextTick(() => { @@ -164,7 +171,7 @@ ref="legendTitleSvg" x="38" :y="graphHeight - 30"> - {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}} + {{createSeriesString(index, series)}} </text> <text v-else diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/graph_path.vue index 043f1bf66bb..043f1bf66bb 100644 --- a/app/assets/javascripts/monitoring/components/monitoring_paths.vue +++ b/app/assets/javascripts/monitoring/components/graph_path.vue diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 05d551e917c..3cbe06d8fd6 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -1,8 +1,37 @@ import d3 from 'd3'; import _ from 'underscore'; -export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) { - const maxValues = seriesData.map((timeSeries, index) => { +const defaultColorPalette = { + blue: ['#1f78d1', '#8fbce8'], + orange: ['#fc9403', '#feca81'], + red: ['#db3b21', '#ed9d90'], + green: ['#1aaa55', '#8dd5aa'], + purple: ['#6666c4', '#d1d1f0'], +}; + +const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; + +export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) { + let usedColors = []; + + function pickColor(name) { + let pick; + if (name && defaultColorPalette[name]) { + pick = name; + } else { + const unusedColors = _.difference(defaultColorOrder, usedColors); + if (unusedColors.length > 0) { + pick = unusedColors[0]; + } else { + usedColors = []; + pick = defaultColorOrder[0]; + } + } + usedColors.push(pick); + return defaultColorPalette[pick]; + } + + const maxValues = queryData.result.map((timeSeries, index) => { const maxValue = d3.max(timeSeries.values.map(d => d.value)); return { maxValue, @@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr const maxValueFromSeries = _.max(maxValues, val => val.maxValue); - let timeSeriesNumber = 1; - let lineColor = '#1f78d1'; - let areaColor = '#8fbce8'; - return seriesData.map((timeSeries) => { + return queryData.result.map((timeSeries, timeSeriesNumber) => { + let metricTag = ''; + let lineColor = ''; + let areaColor = ''; + const timeSeriesScaleX = d3.time.scale() .range([0, graphWidth - 70]); @@ -23,49 +53,30 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr .range([graphHeight - graphHeightOffset, 0]); timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); + timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); const lineFunction = d3.svg.line() + .interpolate('linear') .x(d => timeSeriesScaleX(d.time)) .y(d => timeSeriesScaleY(d.value)); const areaFunction = d3.svg.area() + .interpolate('linear') .x(d => timeSeriesScaleX(d.time)) .y0(graphHeight - graphHeightOffset) - .y1(d => timeSeriesScaleY(d.value)) - .interpolate('linear'); - - switch (timeSeriesNumber) { - case 1: - lineColor = '#1f78d1'; - areaColor = '#8fbce8'; - break; - case 2: - lineColor = '#fc9403'; - areaColor = '#feca81'; - break; - case 3: - lineColor = '#db3b21'; - areaColor = '#ed9d90'; - break; - case 4: - lineColor = '#1aaa55'; - areaColor = '#8dd5aa'; - break; - case 5: - lineColor = '#6666c4'; - areaColor = '#d1d1f0'; - break; - default: - lineColor = '#1f78d1'; - areaColor = '#8fbce8'; - break; - } + .y1(d => timeSeriesScaleY(d.value)); - if (timeSeriesNumber <= 5) { - timeSeriesNumber = timeSeriesNumber += 1; + const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; + const seriesCustomizationData = queryData.series != null && + _.findWhere(queryData.series[0].when, + { value: timeSeriesMetricLabel }); + if (seriesCustomizationData != null) { + metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; + [lineColor, areaColor] = pickColor(seriesCustomizationData.color); } else { - timeSeriesNumber = 1; + metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`; + [lineColor, areaColor] = pickColor(); } return { @@ -75,6 +86,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr values: timeSeries.values, lineColor, areaColor, + metricTag, }; }); } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a09270d6d24..f5f7bb4653d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1272,16 +1272,16 @@ export default class Notes { `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> - <a href="/${currentUsername}"> + <a href="/${_.escape(currentUsername)}"> <img class="avatar s40" src="${currentUserAvatar}" /> </a> </div> <div class="timeline-content ${discussionClass}"> <div class="note-header"> <div class="note-header-info"> - <a href="/${currentUsername}"> - <span class="hidden-xs">${currentUserFullname}</span> - <span class="note-headline-light">@${currentUsername}</span> + <a href="/${_.escape(currentUsername)}"> + <span class="hidden-xs">${_.escape(currentUsername)}</span> + <span class="note-headline-light">${_.escape(currentUsername)}</span> </a> </div> </div> @@ -1295,6 +1295,9 @@ export default class Notes { </li>` ); + $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname)); + $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`); + return $tempNote; } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index a31fedee021..73676bd6de7 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -75,7 +75,7 @@ function UsersSelect(currentUser, els) { if (currentUserInfo) { input.value = currentUserInfo.id; - input.dataset.meta = currentUserInfo.name; + input.dataset.meta = _.escape(currentUserInfo.name); } else if (_this.currentUser) { input.value = _this.currentUser.id; } @@ -198,7 +198,7 @@ function UsersSelect(currentUser, els) { }; } $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', user.name).tooltip('fixTitle'); + $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); }; @@ -506,7 +506,7 @@ function UsersSelect(currentUser, els) { img = ""; if (user.beforeDivider != null) { - `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`; + `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; } else { if (avatar) { img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; @@ -518,7 +518,7 @@ function UsersSelect(currentUser, els) { <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'> ${img} <strong class='dropdown-menu-user-full-name'> - ${user.name} + ${_.escape(user.name)} </strong> ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''} </a> @@ -643,11 +643,11 @@ UsersSelect.prototype.formatResult = function(user) { } else { avatar = gon.default_avatar_url; } - return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>"; + return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + _.escape(user.name) + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>"; }; UsersSelect.prototype.formatSelection = function(user) { - return user.name; + return _.escape(user.name); }; UsersSelect.prototype.user = function(user_id, callback) { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index e20108b171b..5ffa67a1220 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -288,11 +288,7 @@ display: flex; max-width: 350px; overflow: hidden; - - @media(max-width: $screen-xs-max) { - width: 100%; - max-width: none; - } + float: right; .new-project-item-link { white-space: nowrap; @@ -305,6 +301,23 @@ } } +.empty-state .project-item-select-holder.btn-group { + float: none; + display: inline-block; + + .btn { + // overrides styles applied to plain `.empty-state .btn` + margin: 10px 0; + max-width: 300px; + width: auto; + + @media(max-width: $screen-xs-max) { + max-width: 250px; + } + + } +} + .new-project-item-select-button .fa-caret-down { margin-left: 2px; } diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 994e736d66e..3769a2cde33 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController # Authorize before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] - before_action :authorize_create_group!, only: [:new, :create] + before_action :authorize_create_group!, only: [:new] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :group_merge_requests, only: [:merge_requests] @@ -25,14 +25,7 @@ class GroupsController < Groups::ApplicationController end def new - @group = Group.new - - if params[:parent_id].present? - parent = Group.find_by(id: params[:parent_id]) - if can?(current_user, :create_subgroup, parent) - @group.parent = parent - end - end + @group = Group.new(params.permit(:parent_id)) end def create @@ -128,9 +121,14 @@ class GroupsController < Groups::ApplicationController end def authorize_create_group! - unless can?(current_user, :create_group) - return render_404 - end + allowed = if params[:parent_id].present? + parent = Group.find_by(id: params[:parent_id]) + can?(current_user, :create_subgroup, parent) + else + can?(current_user, :create_group) + end + + render_404 unless allowed end def determine_layout diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 9651f9733f9..08fb9db6c0f 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -137,7 +137,7 @@ module CommitsHelper text = if options[:avatar] - %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>} + content_tag(:span, person_name, class: "commit-#{options[:source]}-name") else person_name end @@ -148,9 +148,9 @@ module CommitsHelper } if user.nil? - mail_to(source_email, text.html_safe, options) + mail_to(source_email, text, options) else - link_to(text.html_safe, user_path(user), options) + link_to(text, user_path(user), options) end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 95dbdc8ec46..c4ea0f5ac53 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -104,7 +104,7 @@ module TreeHelper subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) if subtree.count == 1 && subtree.first.dir? - return tree_join(tree.name, flatten_tree(subtree.first)) + return tree_join(tree.name, flatten_tree(root_path, subtree.first)) else return tree.name end diff --git a/app/models/environment.rb b/app/models/environment.rb index 435eeaf0e2e..9b05f8b1cd5 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -82,12 +82,7 @@ class Environment < ActiveRecord::Base def set_environment_type names = name.split('/') - self.environment_type = - if names.many? - names.first - else - nil - end + self.environment_type = names.many? ? names.first : nil end def includes_commit?(commit) @@ -101,7 +96,7 @@ class Environment < ActiveRecord::Base end def update_merge_request_metrics? - (environment_type || name) == "production" + folder_name == "production" end def first_deployment_for(commit) @@ -223,6 +218,10 @@ class Environment < ActiveRecord::Base format: :json) end + def folder_name + self.environment_type || self.name + end + private # Slugifying a name may remove the uniqueness guarantee afforded by it being diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index d9fd6501419..420991ff6d6 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -49,7 +49,7 @@ class GroupPolicy < BasePolicy enable :change_visibility_level end - rule { owner & can_create_group & nested_groups_supported }.enable :create_subgroup + rule { owner & nested_groups_supported }.enable :create_subgroup rule { public_group | logged_in_viewable }.enable :view_globally diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index dcaccc3007d..ba0ae6ba8a0 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -26,5 +26,9 @@ class EnvironmentEntity < Grape::Entity terminal_project_environment_path(environment.project, environment) end + expose :folder_path do |environment| + folder_project_environments_path(environment.project, environment.folder_name) + end + expose :created_at, :updated_at end diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index d0a60f134da..88842a9aa75 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -36,9 +36,9 @@ class EnvironmentSerializer < BaseSerializer private def itemize(resource) - items = resource.order('folder_name ASC') + items = resource.order('folder ASC') .group('COALESCE(environment_type, name)') - .select('COALESCE(environment_type, name) AS folder_name', + .select('COALESCE(environment_type, name) AS folder', 'COUNT(*) AS size', 'MAX(id) AS last_id') # It makes a difference when you call `paginate` method, because @@ -49,7 +49,7 @@ class EnvironmentSerializer < BaseSerializer environments = resource.where(id: items.map(&:last_id)).index_by(&:id) items.map do |item| - Item.new(item.folder_name, item.size, environments[item.last_id]) + Item.new(item.folder, item.size, environments[item.last_id]) end end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index c7c27621085..70e50aa0f12 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -8,15 +8,7 @@ module Groups def execute @group = Group.new(params) - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) - deny_visibility_level(@group) - return @group - end - - if @group.parent && !can?(current_user, :create_subgroup, @group.parent) - @group.parent = nil - @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.') - + unless can_use_visibility_level? && can_create_group? return @group end @@ -39,5 +31,33 @@ module Groups def create_chat_team? Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil? end + + def can_create_group? + if @group.subgroup? + unless can?(current_user, :create_subgroup, @group.parent) + @group.parent = nil + @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.') + + return false + end + else + unless can?(current_user, :create_group) + @group.errors.add(:base, 'You don’t have permission to create groups.') + + return false + end + end + + true + end + + def can_use_visibility_level? + unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + deny_visibility_level(@group) + return false + end + + true + end end end diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml index d0fcd55f6c1..6d6bd79bc3c 100644 --- a/app/views/projects/blob/viewers/_route_map.html.haml +++ b/app/views/projects/blob/viewers/_route_map.html.haml @@ -6,4 +6,4 @@ This Route Map is invalid: = viewer.validation_message -= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map') += link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml index 2318cf82f58..a5f73fb0197 100644 --- a/app/views/projects/blob/viewers/_route_map_loading.html.haml +++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml @@ -1,4 +1,4 @@ = icon('spinner spin fw') Validating Route Map… -= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map') += link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index dc912d800cf..ac2ebb701a5 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,5 +1,5 @@ - if any_projects?(@projects) - .project-item-select-holder.btn-group.pull-right + .project-item-select-holder.btn-group %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } } = icon('spinner spin') = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index c95497dfaba..ec65d3ff65e 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -5,6 +5,9 @@ class GitGarbageCollectWorker sidekiq_options retry: false + # Timeout set to 24h + LEASE_TIMEOUT = 86400 + GITALY_MIGRATED_TASKS = { gc: :garbage_collect, full_repack: :repack_full, @@ -13,8 +16,19 @@ class GitGarbageCollectWorker def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) project = Project.find(project_id) - task = task.to_sym + active_uuid = get_lease_uuid(lease_key) + + if active_uuid + return unless active_uuid == lease_uuid + + renew_lease(lease_key, active_uuid) + else + lease_uuid = try_obtain_lease(lease_key) + + return unless lease_uuid + end + task = task.to_sym cmd = command(task) repo_path = project.repository.path_to_repo description = "'#{cmd.join(' ')}' in #{repo_path}" @@ -33,11 +47,27 @@ class GitGarbageCollectWorker # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc ensure - Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? + cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? end private + def try_obtain_lease(key) + ::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain + end + + def renew_lease(key, uuid) + ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew + end + + def cancel_lease(key, uuid) + ::Gitlab::ExclusiveLease.cancel(key, uuid) + end + + def get_lease_uuid(key) + ::Gitlab::ExclusiveLease.get_uuid(key) + end + ## `repository` has to be a Gitlab::Git::Repository def gitaly_call(task, repository) client = Gitlab::GitalyClient::RepositoryService.new(repository) diff --git a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml new file mode 100644 index 00000000000..76c9c905590 --- /dev/null +++ b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml @@ -0,0 +1,5 @@ +--- +title: Normalize styles for empty state combo button +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-gem-security-updates.yml b/changelogs/unreleased/fix-gem-security-updates.yml deleted file mode 100644 index dce11d08402..00000000000 --- a/changelogs/unreleased/fix-gem-security-updates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade mail and nokogiri gems due to security issues -merge_request: 13662 -author: Markus Koller -type: security diff --git a/changelogs/unreleased/support-additional-colors.yml b/changelogs/unreleased/support-additional-colors.yml new file mode 100644 index 00000000000..5178e159dcf --- /dev/null +++ b/changelogs/unreleased/support-additional-colors.yml @@ -0,0 +1,5 @@ +--- +title: Added support for specific labels and colors +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/zj-update-rails-template.yml b/changelogs/unreleased/zj-update-rails-template.yml new file mode 100644 index 00000000000..5464f0e3d42 --- /dev/null +++ b/changelogs/unreleased/zj-update-rails-template.yml @@ -0,0 +1,5 @@ +--- +title: Update Rails project template to use Postgresql by default +merge_request: +author: +type: changed diff --git a/db/schema.rb b/db/schema.rb index bcb750184db..2149f5ad23d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1712,7 +1712,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade - add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade + add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index a59f71e83a5..b3e7c9bd0bf 100644 --- a/doc/README.md +++ b/doc/README.md @@ -24,7 +24,7 @@ plus premium features available in each version: **Enterprise Edition Starter** Shortcuts to GitLab's most visited docs: -| [GitLab CI](ci/README.md) | Other | +| [GitLab CI/CD](ci/README.md) | Other | | :----- | :----- | | [Quick start guide](ci/quick_start/README.md) | [API](api/README.md) | | [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) | @@ -41,6 +41,7 @@ Shortcuts to GitLab's most visited docs: - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/). - [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown). - [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI. +- [Auto DevOps](topics/autodevops/index.md) ### User account diff --git a/doc/ci/README.md b/doc/ci/README.md index 1bf10e34ae7..5cfd82de381 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -44,6 +44,10 @@ digging into specific reference guides. - [User permissions](../user/permissions.md#gitlab-ci) - [Jobs permissions](../user/permissions.md#jobs-permissions) +## Auto DevOps + +- [Auto DevOps](../topics/autodevops/index.md) + ## GitLab CI + Docker Leverage the power of Docker to run your CI pipelines. diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index a128cf69c20..474cb28b9e4 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -1,13 +1,16 @@ # Auto Deploy ->**Notes:** -- [Introduced][mr-8135] in GitLab 8.15. -- Auto deploy is an experimental feature and is not recommended for Production - use at this time. -- As of GitLab 9.1, access to the Container Registry is only available while - the Pipeline is running. Restarting a pod, scaling a service, or other actions - which require on-going access will fail. On-going secure access is planned for - a subsequent release. +> [Introduced][mr-8135] in GitLab 8.15. +> Auto deploy is an experimental feature and is **not recommended for Production use** at this time. + +> As of GitLab 9.1, access to the container registry is only available while the +Pipeline is running. Restarting a pod, scaling a service, or other actions which +require on-going access **will fail**. On-going secure access is planned for a +subsequent release. + +> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the +functionality has been included in [Auto +DevOps](../../topics/autodevops/index.md). Auto deploy is an easy way to configure GitLab CI for the deployment of your application. GitLab Community maintains a list of `.gitlab-ci.yml` @@ -122,4 +125,3 @@ If you have installed GitLab using a different method: [kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project" [container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html [postgresql]: https://www.postgresql.org/ - diff --git a/doc/ci/environments.md b/doc/ci/environments.md index cbf06afa294..c1362b7bd5b 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -446,8 +446,7 @@ and/or `production`) you can see this information in the merge request itself. ![Environment URLs in merge request](img/environments_link_url_mr.png) -### <a name="route-map"></a>Go directly from source files to public pages on the environment - +### Go directly from source files to public pages on the environment > Introduced in GitLab 8.17. diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index bc75dc1447e..5c128f54a76 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -75,7 +75,7 @@ log_bin_trust_function_creators=1 ### MySQL utf8mb4 support -After installation or upgrade, remember to [convert any new tables](#convert) to `utf8mb4`/`utf8mb4_general_ci`. +After installation or upgrade, remember to [convert any new tables](#tables-and-data-conversion-to-utf8mb4) to `utf8mb4`/`utf8mb4_general_ci`. --- @@ -230,7 +230,6 @@ We need to check, enable and probably convert your existing GitLab DB tables to > Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file. #### Tables and data conversion to utf8mb4 -<a name="convert"></a> Now that you have a persistent MySQL setup, you can safely upgrade tables after setup or upgrade time: diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index a339bc23809..177124c8291 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,9 +1,9 @@ # GitLab Helm Chart -> **Note:** -* > **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). -* Officially supported cloud providers are Google Container Service and Azure Container Service. +> **Note**: +* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). +* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). -The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. For most deployments we recommended the [gitlab-omnibus](gitlab_omnibus.md) chart, +The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment. This chart includes the following: @@ -15,9 +15,11 @@ This chart includes the following: - Optional PostgreSQL deployment using the [PostgreSQL Chart](https://github.com/kubernetes/charts/tree/master/stable/postgresql) (defaults to enabled) - Optional Ingress (defaults to disabled) +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). + ## Prerequisites -- _At least_ 3 GB of RAM available on your cluster, in chunks of 1 GB. 41GB of storage and 2 CPU are also required. +- _At least_ 3 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required. - Kubernetes 1.4+ with Beta APIs enabled - [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure - The ability to point a DNS entry or URL at your GitLab install diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index d7fd8613633..9d1280c3dc6 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -2,13 +2,15 @@ > **Note:** * This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on. * GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these. -* Officially supported cloud providers are Google Container Service and Azure Container Service. +* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). + ## Introduction -This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned as well via [Let's Encrypt](https://letsencrypt.org/). +This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/). The deployment includes: @@ -19,7 +21,13 @@ The deployment includes: - [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) - Persistent Volume Claims for Data, Registry, Postgres, and Redis -A video demonstration of GitLab utilizing this chart [is available](https://about.gitlab.com/handbook/sales/demo/). +### Limitations + +* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported. +* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available. +* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018. + +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). ## Prerequisites @@ -46,7 +54,7 @@ Finally, set the `baseIP` setting to this IP address when [deploying GitLab](#co #### Load Balancer IP -If you do not specify a `baseIP`, an ephemeral IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab: +If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab: `kubectl get svc -w --namespace nginx-ingress nginx` diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index d31c763ed64..5e0d7493b61 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,6 +1,6 @@ # GitLab Runner Helm Chart > **Note:** -Officially supported cloud providers are Google Container Service and Azure Container Service. +These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your Kubernetes cluster. @@ -11,6 +11,8 @@ This chart configures the Runner to: - For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a new pod within the specified namespace to run it. +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). + ## Prerequisites - Your GitLab Server's API is reachable from the cluster diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index fb6c0c2d263..c799f88ad74 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -1,56 +1,59 @@ # Installing GitLab on Kubernetes -> Officially supported cloud providers are Google Container Service and Azure Container Service. +> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). -The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is +The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is to take advantage of GitLab's Helm charts. [Helm] is a package management tool for Kubernetes, allowing apps to be easily managed via their Charts. A [Chart] is a detailed description of the application including how it should be deployed, upgraded, and configured. -GitLab provides [official Helm Charts](#official-gitlab-helm-charts-recommended) which are the recommended way to run GitLab within Kubernetes. +## Chart Overview -There are also two other sets of charts: -* Our [upcoming cloud native Charts](#upcoming-cloud-native-helm-charts), which are in development but will eventually replace the current official charts. -* [Community contributed charts](#community-contributed-helm-charts). These charts should be considered deprecated, in favor of the official charts. +* **[GitLab-Omnibus](#gitlab-omnibus-chart-recommended)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added. +* **[Upcoming Cloud Native Charts](#upcoming-cloud-native-helm-charts)**: The next generation of charts, currently in development. Will support large deployments, with horizontal scaling of individual GitLab components. +* Other Charts + * [GitLab Runner Chart](#gitlab-runner-chart): For deploying just the GitLab Runner. + * [Advanced GitLab Installation](#advanced-gitlab-installation): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options. + * [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab charts. -## Official GitLab Helm Charts +## GitLab-Omnibus Chart (Recommended) +> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added. -These charts utilize our [GitLab Omnibus Docker images](https://docs.gitlab.com/omnibus/docker/README.html). You can report any issues and feedback related to these charts at -https://gitlab.com/charts/charts.gitlab.io/issues. +This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html). -### Deploying GitLab on Kubernetes -> **Note**: This chart will eventually be replaced by the [cloud native charts](#upcoming-cloud-native-helm-charts), which are presently in development. +Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. -The best way to deploy GitLab on Kubernetes is to use the [gitlab-omnibus](gitlab_omnibus.md) chart. +Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md) -It includes everything needed to run GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and an [Ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being completed. +## Upcoming Cloud Native Charts -### Deploying just the GitLab Runner +GitLab is working towards building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended). -To deploy just the [GitLab Runner](https://docs.gitlab.com/runner/), utilize the [gitlab-runner](gitlab_runner_chart.md) chart. +By offering individual containers and charts, we will be able to provide a number of benefits: +* Easier horizontal scaling of each service, +* Smaller, more efficient images, +* Potential for rolling updates and canaries within a service, +* and plenty more. -It offers a quick way to configure and deploy the Runner on Kubernetes, regardless of where your GitLab server may be running. +This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018. -### Advanced deployment of GitLab -> **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). +## Other Charts -If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the GitLab service along with optional Postgres and Redis. It offers extensive configuration, but requires deep knowledge of Kubernetes and Helm to use. +### GitLab Runner Chart -For most deployments we recommend using our [gitlab-omnibus](gitlab_omnibus.md) chart. +If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/executors/kubernetes.md), it can be deployed with the GitLab Runner chart. -## Upcoming Cloud Native Helm Charts +Learn more about [gitlab-runner chart.](gitlab_runner_chart.md) -GitLab is working towards a building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into it's [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended). +### Advanced GitLab Installation -By offering individual containers and charts, we will be able to provide a number of benefits: -* Easier horizontal scaling of each service -* Smaller more efficient images -* Potential for rolling updates and canaries within a service -* and plenty more. +If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the core GitLab service along with optional Postgres and Redis. It offers extensive configuration, but offers limited functionality out-of-the-box; it's lacking Pages support, the container registry, and Mattermost. It requires deep knowledge of Kubernetes and Helm to use. + +This chart will be deprecated and replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). It's beta quality, and since it is not actively under development, it will never be GA. -This is a large project and will be worked on over the span of multiple releases. For the most up to date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). +Learn more about the [gitlab chart.](gitlab_chart.md) -## Community Contributed Helm Charts +### Community Contributed Charts The community has also [contributed GitLab charts](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ce) to the [Helm Stable Repository](https://github.com/kubernetes/charts#repository-structure). These charts should be considered [deprecated](https://github.com/kubernetes/charts/issues/1138) in favor of the [official Charts](#official-gitlab-helm-charts-recommended). diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png Binary files differnew file mode 100644 index 00000000000..57bd7650a30 --- /dev/null +++ b/doc/topics/autodevops/img/auto_devops_settings.png diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png Binary files differnew file mode 100644 index 00000000000..5661b50841b --- /dev/null +++ b/doc/topics/autodevops/img/auto_monitoring.png diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png Binary files differnew file mode 100644 index 00000000000..b856b81a1d0 --- /dev/null +++ b/doc/topics/autodevops/img/guide_connect_cluster.png diff --git a/doc/topics/autodevops/img/guide_integration.png b/doc/topics/autodevops/img/guide_integration.png Binary files differnew file mode 100644 index 00000000000..723b2619ea2 --- /dev/null +++ b/doc/topics/autodevops/img/guide_integration.png diff --git a/doc/topics/autodevops/img/guide_secret.png b/doc/topics/autodevops/img/guide_secret.png Binary files differnew file mode 100644 index 00000000000..01f5aa49908 --- /dev/null +++ b/doc/topics/autodevops/img/guide_secret.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md new file mode 100644 index 00000000000..babf44d2665 --- /dev/null +++ b/doc/topics/autodevops/index.md @@ -0,0 +1,342 @@ +# Auto DevOps + +> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and +**not recommended for production use**. Access to the Container Registry is only +available while the pipeline is running. Restarting a pod, scaling a service, or +other actions which require on-going access **will fail** even for public +projects. On-going secure access is planned for a subsequent release. + +Auto DevOps brings best practices to your project in an easy and default way. A +typical web project starts with Continuous Integration (CI), then adds automated +deployment to production, and maybe some time in the future adds some kind of +monitoring. With Auto DevOps, every project has a complete workflow, with +no configuration, including: + +- [Auto Build](#auto-build) +- [Auto Test](#auto-test) +- [Auto Code Quality](#auto-code-quality) +- [Auto Review Apps](#auto-review-apps) +- [Auto Deploy](#auto-deploy) +- [Auto Monitoring](#-auto-monitoring) + +## Overview + +You will need [Kubernetes](https://kubernetes.io/) and +[Prometheus](https://prometheus.io/) to make full use of Auto DevOps, but +even projects using only [GitLab Runners](https://docs.gitlab.com/runner/) will +be able to make use of Auto Build, Auto Test, and Auto Code Quality. + +Auto DevOps makes use of an open source tool called +[Herokuish](https://github.com/gliderlabs/herokuish) which uses [Heroku +buildpacks](https://devcenter.heroku.com/articles/buildpacks) to automatically +detect, build, and test applications. Auto DevOps supports all of the languages +and frameworks that are [supported by +Herokuish](https://github.com/gliderlabs/herokuish#buildpacks) such as Ruby, +Rails, Node, PHP, Python, and Java, and [custom buildpacks can be +specified](#using-custom-buildpacks). *GitLab is in no way affiliated with Heroku +or Glider Labs.* + +Projects can [customize](#customizing) the process by specifying [custom +buildpacks](#custom-buildpack), [custom `Dockerfile`s](#custom-dockerfile), +[custom Helm charts](#custom-helm-chart), or even copying the complete CI/CD +configuration into your project to enable staging and canary deployments, and +more. + +## Quick start + +If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) +for using Auto DevOps with GitLab.com and an external Kubernetes cluster on +Google Cloud. + +For self-hosted installations, the easiest way to make use of Auto DevOps is to +install GitLab inside a Kubernetes cluster using the [GitLab-Omnibus Helm +Chart](../../install/kubernetes/gitlab_omnibus.md) which automatically installs +and configures everything you need. + +## Prerequisites + +You will need one or more GitLab Runners, a Kubernetes cluster, and Prometheus +installed in the cluster to make full use of Auto DevOps. If you do not have +Kubernetes or Prometheus installed then Auto Review Apps, Auto Deploy, and Auto +Monitoring will be silently skipped. + +If you are using GitLab outside of Kubernetes, for example with GitLab.com, then +you should take these prerequisites into account: + +1. **Base domain** - You will need a base domain configured with wildcard DNS to + be used by all of your Auto DevOps applications. + +1. **GitLab Runner** - Your Runner needs to be configured to be able to run Docker. + Generally this means using the + [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes + executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with + [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode). + The Runners do not need to be installed in the Kubernetes cluster, but the + Kubernetes executor is easy to use and is automatically autoscaling. + Docker-based Runners can be configured to autoscale as well, using [Docker + Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners + should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) + for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner) + that are assigned to specific projects. + +1. **Kubernetes** - To enable deploys, you will need Kubernetes 1.5+, with NGINX + ingress and wildcard SSL termination, for example using the + [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) + and [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego) + Helm charts respectively. The [Kubernetes service][kubernetes-service] + integration will need to be enabled for the project, or enabled as a + [default service template](../../user/project/integrations/services_templates.md) + for the entire GitLab installation. + +1. **Prometheus** - To enable Auto Monitoring, you will need Prometheus installed + somewhere (inside or outside your cluster) and configured to scrape your + Kubernetes cluster. To get response metrics (in addition to system metrics), + you need to [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics). + The [Prometheus service](../../user/project/integrations/prometheus.md) + integration needs to be enabled for the project, or enabled as a + [default service template](../../user/project/integrations/services_templates.md) + for the entire GitLab installation. + +## Enabling Auto DevOps + +In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps +section. Select "Enable Auto DevOps", add in your base domain, and save. + +![auto devops settings](img/auto_devops_settings.png) + +## Stages of Auto DevOps + +The following sections describe the stages of Auto DevOps. + +### Auto Build + +Auto Build creates a build of the application in one of two ways: + +- If there is a `Dockerfile`, it will use `docker build` to create a Docker image. +- Otherwise, it will use [Herokuish](https://github.com/gliderlabs/herokuish) + and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) + to automatically detect and build the application into a Docker image. + +Either way, the resulting Docker image is automatically pushed to the +[Container Registry][container-registry], tagged with the commit SHA. + +### Auto Test + +Auto Test automatically tests your application using +[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku +buildpacks](https://devcenter.heroku.com/articles/buildpacks). Auto Test will +analyze your project to detect the language and framework, and run appropriate +tests. Several languages and frameworks are detected automatically, but if your +language is not detected, you may succeed with a [custom +buildpack](#custom-buildpack). + +Auto Test uses tests you already have in your application. If there are no +tests, it's up to you to add them. + +### Auto Code Quality + +Auto Code Quality uses the open source +[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run +static analysis and other code checks on the current code, creating a report +that is uploaded as an artifact. In GitLab EE, differences between the source +and target branches are shown in the merge request widget. *GitLab is in no way +affiliated with Code Climate.* + +### Auto Review Apps + +Auto Review Apps create a [Review App][review-app] for each branch. Review Apps +are temporary application environments based on the branch's code so developers, +designers, QA, product managers, and other reviewers can actually see and +interact with code changes as part of the review process. + +The review app will have a unique URL based on the project name, the branch +name, and a unique number, combined with the Auto DevOps base domain. For +example, `user-project-branch-1234.example.com`. A link to the Review App shows +up in the merge request widget for easy discovery. When the branch is deleted, +for example after the merge request is merged, the Review App will automatically +be deleted. + +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the Kubernetes service is not configured, or if the variable +`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto +DevOps setting), the job will silently be skipped. + +### Auto Deploy + +After a branch or merge request is merged into `master`, Auto Deploy deploys the +application to a `production` environment in the Kubernetes cluster, with a +namespace based on the project name and unique project ID. e.g. `project-4321`. +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the Kubernetes service is not configured, or if the variable +`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto +DevOps setting), the job will silently be skipped. + +Auto Deploy doesn't include deployments to staging or canary by default, but the +Auto DevOps template contains job definitions for these tasks if you want to +enable them. + +### Auto Monitoring + +Once your application is deployed, Auto Monitoring makes it possible to monitor +your application's server and response metrics right out of the box. Auto +Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to +get system metrics such as CPU and memory usage directly from +[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md), +and response metrics such as HTTP error rates, latency, and throughput from the +[NGINX +server](../../user/project/integrations/prometheus_library/nginx_ingress.md). + +* Response Metrics: latency, throughput, error rate +* System Metrics: CPU utilization, memory utilization + +To view the metrics, open the [Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments). + +![Auto Metrics](img/auto_monitoring.png) + +### Configuring Auto Monitoring + +If GitLab has been deployed using the +[omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no +configuration is required. + +If you have installed GitLab using a different method: + +1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster +1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml). +1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. + +## Customizing + +### PostgreSQL Database Support + +In order to support applications that require a database, +[PostgreSQL][postgresql] is provisioned by default. Credentials to access the +database are preconfigured, but can be customized by setting the associated +[variables](#postgresql-variables). These credentials can be used for defining a +`DATABASE_URL` of the format: +`postgres://user:password@postgres-host:postgres-port/postgres-database`. + +PostgreSQL provisioning can be disabled by creating a project variable +`POSTGRES_ENABLED` set to `false`. + +#### PostgreSQL Variables + +Any variables set at the project or group level will override variables set in +the CI/CD configuration. + +1. `POSTGRES_ENABLED: "false"`: disable automatic deployment of PostgreSQL +1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL +1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL +1. `POSTGRES_DB: "my-database"`: use custom database name for PostgreSQL + +### Custom buildpack + +If the automatic buildpack detection fails for your project, or if you want to +use a custom buildpack, you can override the buildpack using a project variable +or a `.buildpack` file in your project: + +- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL + of the buildpack to use. + +- **`.buildpack` file** - Add a file in your project's repo called `.buildpack` + and add the URL of the buildpack to use on a line in the file. If you want to + use multiple buildpacks, you can enter them in, one on each line + + >**Note:** Using multiple buildpacks may break Auto Test. + +### Custom `Dockerfile` + +If your project has a `Dockerfile` in the root of the project repo, Auto DevOps +will build a Docker image based on the Dockerfile rather than using buildpacks. +This can be much faster and result in smaller images, especially if your +Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/). + +### Custom Helm Chart + +Auto DevOps uses Helm to deploy your application to Kubernetes. You can override +the Helm chart used by bundling up a chart into your project repo or by +specifying a project variable. + +**Bundled chart** - If your project has a `chart` directory with a `Chart.yaml` +file in it, Auto DevOps will detect the chart and use it instead of the default +chart. This can be a great way to control exactly how your application is +deployed. + +**Project variable** - Create a project variable `AUTO_DEVOPS_CHART` with the +URL of a custom chart to use. + +### Enable staging, canaries, and more with custom `.gitlab-ci.yml` + +If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the +Auto DevOps template into your project's repo and edit as you see fit. + +From your project home page, click on the `Set up CI` button, or click on the `+` +button and `New file` and pick `.gitlab-ci.yml` as the template type, or view an +existing `.gitlab-ci.yml` file. Then select "Auto DevOps" from the template +dropdown. You will then be able to edit or add any jobs needed. + +For example, if you want deploys to go to a staging environment instead of +directly to a production environment, you can enable the `staging` job by +renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of +the `production` job to turn it into a manual action instead of deploying +automatically. + +## Currently supported languages + +>**Note:** +Not all buildpacks support Auto Test yet, as it's a relatively new +enhancement. All of Heroku's [officially supported +languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages) +support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP, +Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the +multi-buildpack does not. + +As of GitLab 10.0, the supported buildpacks are: + +``` +* heroku-buildpack-multi v1.0.0 +* heroku-buildpack-ruby v168 +* heroku-buildpack-nodejs v99 +* heroku-buildpack-clojure v77 +* heroku-buildpack-python v99 +* heroku-buildpack-java v53 +* heroku-buildpack-gradle v23 +* heroku-buildpack-scala v78 +* heroku-buildpack-play v26 +* heroku-buildpack-php v122 +* heroku-buildpack-go v72 +* heroku-buildpack-erlang fa17af9 +* buildpack-nginx v8 +``` + +## Private Project Support - Experimental + +When a project has been marked as private, GitLab's [Container +Registry][container-registry] requires authentication when downloading +containers. Auto DevOps will automatically provide the required authentication +information to Kubernetes, allowing temporary access to the registry. +Authentication credentials will be valid while the pipeline is running, allowing +for a successful initial deployment. + +After the pipeline completes, Kubernetes will no longer be able to access the +container registry. **Restarting a pod, scaling a service, or other actions which +require on-going access to the registry will fail**. On-going secure access is +planned for a subsequent release. + +## Troubleshooting + +- Auto Build and Auto Test may fail in detecting your language/framework. There + may be no buildpack for your application, or your application may be missing the + key files the buildpack is looking for. For example, for ruby apps, you must + have a `Gemfile` to be properly detected, even though it is possible to write a + Ruby app without a `Gemfile`. Try specifying a [custom + buildpack](#custom-buildpack). +- Auto Test may fail because of a mismatch between testing frameworks. In this + case, you may need to customize your `.gitlab-ci.yml` with your test commands. + +[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 +[kubernetes-service]: ../../user/project/integrations/kubernetes.md +[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../../ci/review_apps/index.md +[container-registry]: ../../user/project/container_registry.md +[postgresql]: https://www.postgresql.org/ diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md new file mode 100644 index 00000000000..f23c9d794b4 --- /dev/null +++ b/doc/topics/autodevops/quick_start_guide.md @@ -0,0 +1,95 @@ +# Auto DevOps: quick start guide + +> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and +**not recommended for production use**. + +This is a step-by-step guide to deploying a project hosted on GitLab.com to +Google Cloud, using Auto DevOps. + +We made a minimal [Ruby +application](https://gitlab.com/gitlab-examples/minimal-ruby-app) to use as an +example for this guide. It contains two files: + +* `server.rb` - our application. It will start an HTTP server on port 5000 and + render "Hello, world!" +* `Dockerfile` - to build our app into a container image. It will use a ruby + base image and run `server.rb` + +## Fork sample project on GitLab.com + +Let’s start by forking our sample application. Go to [the project +page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` +button. Soon you should have a project under your namespace with the necessary +files. + +## Setup your own cluster on Google Container Engine + +If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. + +Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. + +## Connect to Kubernetes cluster + +You need to have the Google Cloud SDK installed. e.g. +On OSX, install [homebrew](https://brew.sh): + +1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask` +2. Install Google Cloud SDK: `brew cask install google-cloud-sdk` +3. Add `kubectl`: `gcloud components install kubectl` +4. Log in: `gcloud auth login` + +Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`. + +![connect to cluster](img/guide_connect_cluster.png) + +## Copy credentials to GitLab.com project + +Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token. + +![connect to cluster](img/guide_secret.png) + +You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you. + +![connect to cluster](img/guide_integration.png) + +For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform. + +## Expose application to the world + +In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address. + +### Set up Ingress controller + +You’ll need to make sure you have an ingress controller. If you don’t have one, do: + +```sh +brew install kubernetes-helm +helm init +helm install --name ruby-app stable/nginx-ingress +``` + +This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that. + +### Point DNS at Cluster IP + +Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running: + +```sh +kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' +``` + +Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>`. + +Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP. + +## Set up Auto DevOps + +In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save. + +![auto devops settings](img/auto_devops_settings.png) + +Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab. + +Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!” + +[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 diff --git a/doc/topics/index.md b/doc/topics/index.md index ad388dff822..b51f24b02e4 100644 --- a/doc/topics/index.md +++ b/doc/topics/index.md @@ -7,6 +7,7 @@ you through better understanding GitLab's concepts through our regular docs, and, when available, through articles (guides, tutorials, technical overviews, blog posts) and videos. +- [Auto DevOps](autodevops/index.md) - [Authentication](authentication/index.md) - [Continuous Integration (GitLab CI)](../ci/README.md) - [Git](git/index.md) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 31a918eda60..e817dcbbc4b 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -74,7 +74,12 @@ module API use :optional_params end post do - authorize! :create_group + parent_group = find_group!(params[:parent_id]) if params[:parent_id].present? + if parent_group + authorize! :create_subgroup, parent_group + else + authorize! :create_group + end group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8b03df65ae4..00dbc2aee7a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -93,7 +93,7 @@ module API end def find_group(id) - if id =~ /^\d+$/ + if id.to_s =~ /^\d+$/ Group.find_by(id: id) else Group.find_by_full_path(id) diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 2d6e8ffc90f..9923ec4e870 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -5,6 +5,7 @@ module Banzai # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. class SanitizationFilter < HTML::Pipeline::SanitizationFilter UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze + TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/ def whitelist whitelist = super @@ -24,7 +25,8 @@ module Banzai # Only push these customizations once return if customized?(whitelist[:transformers]) - # Allow table alignment + # Allow table alignment; we whitelist specific style properties in a + # transformer below whitelist[:attributes]['th'] = %w(style) whitelist[:attributes]['td'] = %w(style) @@ -43,6 +45,9 @@ module Banzai whitelist[:elements].push('abbr') whitelist[:attributes]['abbr'] = %w(title) + # Disallow `name` attribute globally + whitelist[:attributes][:all].delete('name') + # Allow any protocol in `a` elements... whitelist[:protocols].delete('a') @@ -52,6 +57,9 @@ module Banzai # Remove `rel` attribute from `a` elements whitelist[:transformers].push(self.class.remove_rel) + # Remove any `style` properties not required for table alignment + whitelist[:transformers].push(self.class.remove_unsafe_table_style) + whitelist end @@ -81,6 +89,21 @@ module Banzai end end end + + def remove_unsafe_table_style + lambda do |env| + node = env[:node] + + return unless node.name == 'th' || node.name == 'td' + return unless node.has_attribute?('style') + + if node['style'] =~ TABLE_ALIGNMENT_PATTERN + node['style'] = "text-align: #{$~[:alignment]}" + else + node.remove_attribute('style') + end + end + end end end end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 3784f6c4947..3f7b42456af 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -25,6 +25,12 @@ module Gitlab end EOS + def self.get_uuid(key) + Gitlab::Redis::SharedState.with do |redis| + redis.get(redis_shared_state_key(key)) || false + end + end + def self.cancel(key, uuid) Gitlab::Redis::SharedState.with do |redis| redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_shared_state_key(key)], argv: [uuid]) @@ -35,10 +41,10 @@ module Gitlab "gitlab:exclusive_lease:#{key}" end - def initialize(key, timeout:) + def initialize(key, uuid: nil, timeout:) @redis_shared_state_key = self.class.redis_shared_state_key(key) @timeout = timeout - @uuid = SecureRandom.uuid + @uuid = uuid || SecureRandom.uuid end # Try to obtain the lease. Return lease UUID on success, diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 6023fa1820f..f42168c720e 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -3,6 +3,10 @@ module Gitlab module Middleware class Go + include ActionView::Helpers::TagHelper + + PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze + def initialize(app) @app = app end @@ -10,17 +14,20 @@ module Gitlab def call(env) request = Rack::Request.new(env) - if go_request?(request) - render_go_doc(request) - else - @app.call(env) - end + render_go_doc(request) || @app.call(env) end private def render_go_doc(request) - body = go_body(request) + return unless go_request?(request) + + path = project_path(request) + return unless path + + body = go_body(path) + return unless body + response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' }) response.finish end @@ -29,11 +36,13 @@ module Gitlab request["go-get"].to_i == 1 && request.env["PATH_INFO"].present? end - def go_body(request) - project_url = URI.join(Gitlab.config.gitlab.url, project_path(request)) + def go_body(path) + project_url = URI.join(Gitlab.config.gitlab.url, path) import_prefix = strip_url(project_url.to_s) - "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n" + meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git" + head_tag = content_tag :head, meta_tag + content_tag :html, head_tag end def strip_url(url) @@ -44,6 +53,10 @@ module Gitlab path_info = request.env["PATH_INFO"] path_info.sub!(/^\//, '') + project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX) + return unless project_path_match + path = project_path_match[1] + # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`. # In a traditional project with a single namespace, this would denote repo # `namespace/project` with subpath `path1/path2/../pathN`, but with nested @@ -51,7 +64,7 @@ module Gitlab # `path2/../pathN`, for example. # We find all potential project paths out of the path segments - path_segments = path_info.split('/') + path_segments = path.split('/') simple_project_path = path_segments.first(2).join('/') # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index c2ada8c8df7..b0564e27a68 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -2,9 +2,133 @@ require 'rails_helper' describe GroupsController do let(:user) { create(:user) } + let(:admin) { create(:admin) } let(:group) { create(:group, :public) } let(:project) { create(:project, namespace: group) } let!(:group_member) { create(:group_member, group: group, user: user) } + let!(:owner) { group.add_owner(create(:user)).user } + let!(:master) { group.add_master(create(:user)).user } + let!(:developer) { group.add_developer(create(:user)).user } + let!(:guest) { group.add_guest(create(:user)).user } + + shared_examples 'member with ability to create subgroups' do + it 'renders the new page' do + sign_in(member) + + get :new, parent_id: group.id + + expect(response).to render_template(:new) + end + end + + shared_examples 'member without ability to create subgroups' do + it 'renders the 404 page' do + sign_in(member) + + get :new, parent_id: group.id + + expect(response).not_to render_template(:new) + expect(response.status).to eq(404) + end + end + + describe 'GET #new' do + context 'when creating subgroups', :nested_groups do + [true, false].each do |can_create_group_status| + context "and can_create_group is #{can_create_group_status}" do + before do + User.where(id: [admin, owner, master, developer, guest]).update_all(can_create_group: can_create_group_status) + end + + [:admin, :owner].each do |member_type| + context "and logged in as #{member_type.capitalize}" do + it_behaves_like 'member with ability to create subgroups' do + let(:member) { send(member_type) } + end + end + end + + [:guest, :developer, :master].each do |member_type| + context "and logged in as #{member_type.capitalize}" do + it_behaves_like 'member without ability to create subgroups' do + let(:member) { send(member_type) } + end + end + end + end + end + end + end + + describe 'POST #create' do + context 'when creating subgroups', :nested_groups do + [true, false].each do |can_create_group_status| + context "and can_create_group is #{can_create_group_status}" do + context 'and logged in as Owner' do + it 'creates the subgroup' do + owner.update_attribute(:can_create_group, can_create_group_status) + sign_in(owner) + + post :create, group: { parent_id: group.id, path: 'subgroup' } + + expect(response).to be_redirect + expect(response.body).to match(%r{http://test.host/#{group.path}/subgroup}) + end + end + + context 'and logged in as Developer' do + it 'renders the new template' do + developer.update_attribute(:can_create_group, can_create_group_status) + sign_in(developer) + + previous_group_count = Group.count + + post :create, group: { parent_id: group.id, path: 'subgroup' } + + expect(response).to render_template(:new) + expect(Group.count).to eq(previous_group_count) + end + end + end + end + end + + context 'when creating a top level group' do + before do + sign_in(developer) + end + + context 'and can_create_group is enabled' do + before do + developer.update_attribute(:can_create_group, true) + end + + it 'creates the Group' do + original_group_count = Group.count + + post :create, group: { path: 'subgroup' } + + expect(Group.count).to eq(original_group_count + 1) + expect(response).to be_redirect + end + end + + context 'and can_create_group is disabled' do + before do + developer.update_attribute(:can_create_group, false) + end + + it 'does not create the Group' do + original_group_count = Group.count + + post :create, group: { path: 'subgroup' } + + expect(Group.count).to eq(original_group_count) + expect(response).to render_template(:new) + end + end + end + end describe 'GET #index' do context 'as a user' do diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 3e6a27eafd8..dfeba722ac6 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -288,8 +288,6 @@ describe 'Copy as GFM', js: true do 'SanitizationFilter', <<-GFM.strip_heredoc - <a name="named-anchor"></a> - <sub>sub</sub> <dl> diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 1c59e57c0a4..af7ad365546 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -10,26 +10,23 @@ feature 'Environments page', :js do sign_in(user) end - given!(:environment) { } - given!(:deployment) { } - given!(:action) { } - - before do - visit_environments(project) - end - describe 'page tabs' do - scenario 'shows "Available" and "Stopped" tab with links' do + it 'shows "Available" and "Stopped" tab with links' do + visit_environments(project) + expect(page).to have_link('Available') expect(page).to have_link('Stopped') end describe 'with one available environment' do - given(:environment) { create(:environment, project: project, state: :available) } + before do + create(:environment, project: project, state: :available) + end describe 'in available tab page' do it 'should show one environment' do - visit project_environments_path(project, scope: 'available') + visit_environments(project, scope: 'available') + expect(page).to have_css('.environments-container') expect(page.all('.environment-name').length).to eq(1) end @@ -37,7 +34,8 @@ feature 'Environments page', :js do describe 'in stopped tab page' do it 'should show no environments' do - visit project_environments_path(project, scope: 'stopped') + visit_environments(project, scope: 'stopped') + expect(page).to have_css('.environments-container') expect(page).to have_content('You don\'t have any environments right now') end @@ -45,11 +43,14 @@ feature 'Environments page', :js do end describe 'with one stopped environment' do - given(:environment) { create(:environment, project: project, state: :stopped) } + before do + create(:environment, project: project, state: :stopped) + end describe 'in available tab page' do it 'should show no environments' do - visit project_environments_path(project, scope: 'available') + visit_environments(project, scope: 'available') + expect(page).to have_css('.environments-container') expect(page).to have_content('You don\'t have any environments right now') end @@ -57,7 +58,8 @@ feature 'Environments page', :js do describe 'in stopped tab page' do it 'should show one environment' do - visit project_environments_path(project, scope: 'stopped') + visit_environments(project, scope: 'stopped') + expect(page).to have_css('.environments-container') expect(page.all('.environment-name').length).to eq(1) end @@ -66,86 +68,84 @@ feature 'Environments page', :js do end context 'without environments' do - scenario 'does show no environments' do - expect(page).to have_content('You don\'t have any environments right now.') + before do + visit_environments(project) end - scenario 'does show 0 as counter for environments in both tabs' do + it 'does not show environments and counters are set to zero' do + expect(page).to have_content('You don\'t have any environments right now.') + expect(page.find('.js-available-environments-count').text).to eq('0') expect(page.find('.js-stopped-environments-count').text).to eq('0') end end - describe 'when showing the environment' do - given(:environment) { create(:environment, project: project) } - - scenario 'does show environment name' do - expect(page).to have_link(environment.name) - end - - scenario 'does show number of available and stopped environments' do - expect(page.find('.js-available-environments-count').text).to eq('1') - expect(page.find('.js-stopped-environments-count').text).to eq('0') + describe 'environments table' do + given!(:environment) do + create(:environment, project: project, state: :available) end - context 'without deployments' do - scenario 'does show no deployments' do - expect(page).to have_content('No deployments yet') + context 'when there are no deployments' do + before do + visit_environments(project) end - context 'for available environment' do - given(:environment) { create(:environment, project: project, state: :available) } + it 'shows environments names and counters' do + expect(page).to have_link(environment.name) - scenario 'does not shows stop button' do - expect(page).not_to have_selector('.stop-env-link') - end + expect(page.find('.js-available-environments-count').text).to eq('1') + expect(page.find('.js-stopped-environments-count').text).to eq('0') end - context 'for stopped environment' do - given(:environment) { create(:environment, project: project, state: :stopped) } + it 'does not show deployments' do + expect(page).to have_content('No deployments yet') + end - scenario 'does not shows stop button' do - expect(page).not_to have_selector('.stop-env-link') - end + it 'does not show stip button when environment is not stoppable' do + expect(page).not_to have_selector('.stop-env-link') end end - context 'with deployments' do + context 'when there are deployments' do given(:project) { create(:project, :repository) } - given(:deployment) do + given!(:deployment) do create(:deployment, environment: environment, sha: project.commit.id) end - scenario 'does show deployment SHA' do - expect(page).to have_link(deployment.short_sha) - end + it 'shows deployment SHA and internal ID' do + visit_environments(project) - scenario 'does show deployment internal id' do + expect(page).to have_link(deployment.short_sha) expect(page).to have_content(deployment.iid) end - context 'with build and manual actions' do - given(:pipeline) { create(:ci_pipeline, project: project) } - given(:build) { create(:ci_build, pipeline: pipeline) } + context 'when builds and manual actions are present' do + given!(:pipeline) { create(:ci_pipeline, project: project) } + given!(:build) { create(:ci_build, pipeline: pipeline) } - given(:action) do + given!(:action) do create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') end - given(:deployment) do + given!(:deployment) do create(:deployment, environment: environment, deployable: build, sha: project.commit.id) end - scenario 'does show a play button' do + before do + visit_environments(project) + end + + it 'shows a play button' do find('.js-dropdown-play-icon-container').click + expect(page).to have_content(action.name.humanize) end - scenario 'does allow to play manual action', js: true do + it 'allows to play a manual action', js: true do expect(action).to be_manual find('.js-dropdown-play-icon-container').click @@ -155,19 +155,19 @@ feature 'Environments page', :js do .not_to change { Ci::Pipeline.count } end - scenario 'does show build name and id' do + it 'shows build name and id' do expect(page).to have_link("#{build.name} ##{build.id}") end - scenario 'does not show stop button' do + it 'shows a stop button' do expect(page).not_to have_selector('.stop-env-link') end - scenario 'does not show external link button' do + it 'does not show external link button' do expect(page).not_to have_css('external-url') end - scenario 'does not show terminal button' do + it 'does not show terminal button' do expect(page).not_to have_terminal_button end @@ -176,7 +176,7 @@ feature 'Environments page', :js do given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - scenario 'does show an external link button' do + it 'shows an external link button' do expect(page).to have_link(nil, href: environment.external_url) end end @@ -192,34 +192,34 @@ feature 'Environments page', :js do on_stop: 'close_app') end - scenario 'does show stop button' do + it 'shows a stop button' do expect(page).to have_selector('.stop-env-link') end - context 'for reporter' do + context 'when user is a reporter' do let(:role) { :reporter } - scenario 'does not show stop button' do + it 'does not show stop button' do expect(page).not_to have_selector('.stop-env-link') end end end - context 'with terminal' do + context 'when kubernetes terminal is available' do let(:project) { create(:kubernetes_project, :test_repo) } context 'for project master' do let(:role) { :master } - scenario 'it shows the terminal button' do + it 'shows the terminal button' do expect(page).to have_terminal_button end end - context 'for developer' do + context 'when user is a developer' do let(:role) { :developer } - scenario 'does not show terminal button' do + it 'does not show terminal button' do expect(page).not_to have_terminal_button end end @@ -228,59 +228,77 @@ feature 'Environments page', :js do end end - scenario 'does have a New environment button' do + it 'does have a new environment button' do + visit_environments(project) + expect(page).to have_link('New environment') end - describe 'when creating a new environment' do + describe 'creating a new environment' do before do visit_environments(project) end - context 'when logged as developer' do - before do - within(".top-area") do - click_link 'New environment' - end - end + context 'user is a developer' do + given(:role) { :developer } - context 'for valid name' do - before do - fill_in('Name', with: 'production') - click_on 'Save' - end + scenario 'developer creates a new environment with a valid name' do + within(".top-area") { click_link 'New environment' } + fill_in('Name', with: 'production') + click_on 'Save' - scenario 'does create a new pipeline' do - expect(page).to have_content('production') - end + expect(page).to have_content('production') end - context 'for invalid name' do - before do - fill_in('Name', with: 'name,with,commas') - click_on 'Save' - end + scenario 'developer creates a new environmetn with invalid name' do + within(".top-area") { click_link 'New environment' } + fill_in('Name', with: 'name,with,commas') + click_on 'Save' - scenario 'does show errors' do - expect(page).to have_content('Name can contain only letters') - end + expect(page).to have_content('Name can contain only letters') end end - context 'when logged as reporter' do + context 'user is a reporter' do given(:role) { :reporter } - scenario 'does not have a New environment link' do + scenario 'reporters tries to create a new environment' do expect(page).not_to have_link('New environment') end end end + describe 'environments folders' do + before do + create(:environment, project: project, + name: 'staging/review-1', + state: :available) + create(:environment, project: project, + name: 'staging/review-2', + state: :available) + end + + scenario 'users unfurls an environment folder' do + visit_environments(project) + + expect(page).not_to have_content 'review-1' + expect(page).not_to have_content 'review-2' + expect(page).to have_content 'staging 2' + + within('.folder-row') do + find('.folder-name', text: 'staging').click + end + + expect(page).to have_content 'review-1' + expect(page).to have_content 'review-2' + end + end + def have_terminal_button have_link(nil, href: terminal_project_environment_path(project, environment)) end - def visit_environments(project) - visit project_environments_path(project) + def visit_environments(project, **opts) + visit project_environments_path(project, **opts) end end diff --git a/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb b/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb new file mode 100644 index 00000000000..a17e65cc5b9 --- /dev/null +++ b/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +# This is a regression test for https://gitlab.com/gitlab-org/gitlab-ce/issues/37569 +describe 'User browses a tree with a folder containing only a folder' do + let(:project) { create(:project, :empty_repo) } + let(:user) { project.creator } + + before do + # We need to disable the tree.flat_path provided by Gitaly to reproduce the issue + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + + project.repository.create_dir(user, 'foo/bar', branch_name: 'master', message: 'Add the foo/bar folder') + sign_in(user) + visit(project_tree_path(project, project.repository.root_ref)) + end + + it 'shows the nested folder on a single row' do + expect(page).to have_content('foo/bar') + end +end diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index 7179185285c..4b6c7c33e5b 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -12,6 +12,17 @@ describe CommitsHelper do expect(helper.commit_author_link(commit)) .not_to include('onmouseover="alert(1)"') end + + it 'escapes the author name' do + user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>') + + commit = double(author: user, author_name: '', author_email: '') + + expect(helper.commit_author_link(commit)) + .to include('Foo <script>') + expect(helper.commit_author_link(commit, avatar: true)) + .to include('commit-author-name', 'Foo <script>') + end end describe 'commit_committer_link' do @@ -25,6 +36,17 @@ describe CommitsHelper do expect(helper.commit_committer_link(commit)) .not_to include('onmouseover="alert(1)"') end + + it 'escapes the commiter name' do + user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>') + + commit = double(committer: user, committer_name: '', committer_email: '') + + expect(helper.commit_committer_link(commit)) + .to include('Foo <script>') + expect(helper.commit_committer_link(commit, avatar: true)) + .to include('commit-committer-name', 'Foo <script>') + end end describe '#view_on_environment_button' do diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js index da2fbd26e23..2571b7ef869 100644 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ b/spec/javascripts/monitoring/graph/legend_spec.js @@ -28,7 +28,7 @@ const defaultValuesComponent = { currentDataIndex: 0, }; -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight, defaultValuesComponent.graphHeightOffset); @@ -89,13 +89,12 @@ describe('GraphLegend', () => { expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); }); - it('contains text to signal the usage, title and time', () => { + it('contains text to signal the usage, title and time with multiple time series', () => { const component = createComponent(defaultValuesComponent); const titles = component.$el.querySelectorAll('.legend-metric-title'); - expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1); - expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1); - expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1); + expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); + expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel); }); diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/graph_path_spec.js index d39db945e17..a4844636d09 100644 --- a/spec/javascripts/monitoring/monitoring_paths_spec.js +++ b/spec/javascripts/monitoring/graph_path_spec.js @@ -1,10 +1,10 @@ import Vue from 'vue'; -import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue'; +import GraphPath from '~/monitoring/components/graph_path.vue'; import createTimeSeries from '~/monitoring/utils/multiple_time_series'; import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringPaths); + const Component = Vue.extend(GraphPath); return new Component({ propsData, @@ -13,22 +13,23 @@ const createComponent = (propsData) => { const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120); +const firstTimeSeries = timeSeries[0]; describe('Monitoring Paths', () => { it('renders two paths to represent a line and the area underneath it', () => { const component = createComponent({ - generatedLinePath: timeSeries[0].linePath, - generatedAreaPath: timeSeries[0].areaPath, - lineColor: '#ccc', - areaColor: '#fff', + generatedLinePath: firstTimeSeries.linePath, + generatedAreaPath: firstTimeSeries.areaPath, + lineColor: firstTimeSeries.lineColor, + areaColor: firstTimeSeries.areaColor, }); const metricArea = component.$el.querySelector('.metric-area'); const metricLine = component.$el.querySelector('.metric-line'); - expect(metricArea.getAttribute('fill')).toBe('#fff'); - expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath); - expect(metricLine.getAttribute('stroke')).toBe('#ccc'); - expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath); + expect(metricArea.getAttribute('fill')).toBe('#8fbce8'); + expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath); + expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); + expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 3d399f2bb95..7ceab657464 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -6346,7 +6346,13 @@ export const singleRowMetricsMultipleSeries = [ } ] }, - ] + ], + 'when': [ + { + 'value': 'hundred(s)', + 'color': 'green', + }, + ], } ] }, diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js index 3daf6bf82df..7e44a9ade9e 100644 --- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js +++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js @@ -2,16 +2,17 @@ import createTimeSeries from '~/monitoring/utils/multiple_time_series'; import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data'; const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120); +const firstTimeSeries = timeSeries[0]; describe('Multiple time series', () => { it('createTimeSeries returned array contains an object for each element', () => { - expect(typeof timeSeries[0].linePath).toEqual('string'); - expect(typeof timeSeries[0].areaPath).toEqual('string'); - expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function'); - expect(typeof timeSeries[0].areaColor).toEqual('string'); - expect(typeof timeSeries[0].lineColor).toEqual('string'); - expect(timeSeries[0].values instanceof Array).toEqual(true); + expect(typeof firstTimeSeries.linePath).toEqual('string'); + expect(typeof firstTimeSeries.areaPath).toEqual('string'); + expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function'); + expect(typeof firstTimeSeries.areaColor).toEqual('string'); + expect(typeof firstTimeSeries.lineColor).toEqual('string'); + expect(firstTimeSeries.values instanceof Array).toEqual(true); }); it('createTimeSeries returns an array', () => { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 8c5ad8914b0..3e791a31604 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -770,6 +770,20 @@ import '~/notes'; expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); + + it('should return a escaped user name', () => { + const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>'; + const $tempNote = this.notes.createPlaceholderNote({ + formContent: sampleComment, + uniqueId, + isDiscussionNote: false, + currentUsername, + currentUserFullname: currentUserFullnameXSS, + currentUserAvatar, + }); + const $tempNoteHeader = $tempNote.find('.note-header'); + expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo <script>alert("XSS")</script>'); + }); }); describe('createPlaceholderSystemNote', () => { diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 35a32a46eff..01ceb21dfaa 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -49,7 +49,7 @@ describe Banzai::Filter::SanitizationFilter do instance = described_class.new('Foo') 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 4 + expect(instance.whitelist[:transformers].size).to eq 5 end it 'sanitizes `class` attribute from all elements' do @@ -63,8 +63,8 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq %q{<span>def</span>} end - it 'allows `style` attribute on table elements' do - html = <<-HTML.strip_heredoc + it 'allows `text-align` property in `style` attribute on table elements' do + html = <<~HTML <table> <tr><th style="text-align: center">Head</th></tr> <tr><td style="text-align: right">Body</th></tr> @@ -77,6 +77,20 @@ describe Banzai::Filter::SanitizationFilter do expect(doc.at_css('td')['style']).to eq 'text-align: right' end + it 'disallows other properties in `style` attribute on table elements' do + html = <<~HTML + <table> + <tr><th style="text-align: foo">Head</th></tr> + <tr><td style="position: fixed; height: 50px; width: 50px; background: red; z-index: 999; font-size: 36px; text-align: center">Body</th></tr> + </table> + HTML + + doc = filter(html) + + expect(doc.at_css('th')['style']).to be_nil + expect(doc.at_css('td')['style']).to eq 'text-align: center' + end + it 'allows `span` elements' do exp = act = %q{<span>Hello</span>} expect(filter(act).to_html).to eq exp @@ -87,6 +101,18 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq exp end + it 'disallows the `name` attribute globally' do + html = <<~HTML + <img name="getElementById" src=""> + <span name="foo" class="bar">Hi</span> + HTML + + doc = filter(html) + + expect(doc.at_css('img')).not_to have_attribute('name') + expect(doc.at_css('span')).not_to have_attribute('name') + end + it 'allows `summary` elements' do exp = act = '<summary>summary line</summary>' expect(filter(act).to_html).to eq exp diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index c1ed47cf64a..7322a326b01 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -47,6 +47,18 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do end end + describe '.get_uuid' do + it 'gets the uuid if lease with the key associated exists' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain + + expect(described_class.get_uuid(unique_key)).to eq(uuid) + end + + it 'returns false if the lease does not exist' do + expect(described_class.get_uuid(unique_key)).to be false + end + end + describe '.cancel' do it 'can cancel a lease' do uuid = new_lease(unique_key) diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 6af1564da19..cab662819ac 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -79,12 +79,28 @@ describe Gitlab::Middleware::Go do it_behaves_like 'a nested project' end + context 'with a subpackage that is not a valid project path' do + let(:path) { "#{project.full_path}/---subpackage" } + + it_behaves_like 'a nested project' + end + context 'without subpackages' do let(:path) { project.full_path } it_behaves_like 'a nested project' end end + + context 'with a bogus path' do + let(:path) { "http:;url=http://www.example.com'http-equiv='refresh'x='?go-get=1" } + + it 'skips go-import generation' do + expect(app).to receive(:call).and_return('no-go') + + go + end + end end def go @@ -100,7 +116,7 @@ describe Gitlab::Middleware::Go do def expect_response_with_path(response, path) expect(response[0]).to eq(200) expect(response[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n" + expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>} expect(response[2].body).to eq([expected_body]) end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index ea8512a5eae..25e5d155894 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -54,6 +54,28 @@ describe Environment do end end + describe '#folder_name' do + context 'when it is inside a folder' do + subject(:environment) do + create(:environment, name: 'staging/review-1') + end + + it 'returns a top-level folder name' do + expect(environment.folder_name).to eq 'staging' + end + end + + context 'when the environment if a top-level item itself' do + subject(:environment) do + create(:environment, name: 'production') + end + + it 'returns an environment name' do + expect(environment.folder_name).to eq 'production' + end + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 0c4044dc7ab..b186a78e44a 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -24,8 +24,8 @@ describe GroupPolicy do :admin_namespace, :admin_group_member, :change_visibility_level, - :create_subgroup - ] + (Gitlab::Database.postgresql? ? :create_subgroup : nil) + ].compact end before do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 77c43f92456..42f0079e173 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -431,6 +431,30 @@ describe API::Groups do expect(response).to have_http_status(403) end + + context 'as owner', :nested_groups do + before do + group2.add_owner(user1) + end + + it 'can create subgroups' do + post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo' + + expect(response).to have_http_status(201) + end + end + + context 'as master', :nested_groups do + before do + group2.add_master(user1) + end + + it 'cannot create subgroups' do + post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo' + + expect(response).to have_http_status(403) + end + end end context "when authenticated as user with group permissions" do diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index 979d9921941..8f32c5639a1 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -16,6 +16,10 @@ describe EnvironmentEntity do expect(subject).to include(:id, :name, :state, :environment_path) end + it 'exposes folder path' do + expect(subject).to include(:folder_path) + end + context 'metrics disabled' do before do allow(environment).to receive(:has_metrics?).and_return(false) diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 10dda45d2a1..224e933bebc 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -22,6 +22,26 @@ describe Groups::CreateService, '#execute' do end end + describe 'creating a top level group' do + let(:service) { described_class.new(user, group_params) } + + context 'when user can create a group' do + before do + user.update_attribute(:can_create_group, true) + end + + it { is_expected.to be_persisted } + end + + context 'when user can not create a group' do + before do + user.update_attribute(:can_create_group, false) + end + + it { is_expected.not_to be_persisted } + end + end + describe 'creating subgroup', :nested_groups do let!(:group) { create(:group) } let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } @@ -44,13 +64,26 @@ describe Groups::CreateService, '#execute' do end end - context 'as guest' do - it 'does not save group and returns an error' do + context 'when nested groups feature is enabled' do + before do allow(Group).to receive(:supports_nested_groups?).and_return(true) + end + + context 'as guest' do + it 'does not save group and returns an error' do + is_expected.not_to be_persisted + + expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.') + expect(subject.parent_id).to be_nil + end + end + + context 'as owner' do + before do + group.add_owner(user) + end - is_expected.not_to be_persisted - expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.') - expect(subject.parent_id).to be_nil + it { is_expected.to be_persisted } end end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 9386c110385..437c009e7fa 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -20,6 +20,7 @@ describe Projects::HousekeepingService do expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid) subject.execute + expect(project.reload.pushes_since_gc).to eq(0) end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index c4979792194..6f9ddb6c63c 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -5,28 +5,100 @@ require 'spec_helper' describe GitGarbageCollectWorker do let(:project) { create(:project, :repository) } let(:shell) { Gitlab::Shell.new } + let!(:lease_uuid) { SecureRandom.uuid } + let!(:lease_key) { "project_housekeeping:#{project.id}" } subject { described_class.new } describe "#perform" do shared_examples 'flushing ref caches' do |gitaly| - it "flushes ref caches when the task if 'gc'" do - expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - - if gitaly - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) - .and_return(nil) - else - expect(Gitlab::Popen).to receive(:popen) - .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + context 'with active lease_uuid' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) end - expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original - expect_any_instance_of(Repository).to receive(:branch_names).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + it "flushes ref caches when the task if 'gc'" do + expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original + expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - subject.perform(project.id) + if gitaly + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) + .and_return(nil) + else + expect(Gitlab::Popen).to receive(:popen) + .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + end + + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + + subject.perform(project.id, :gc, lease_key, lease_uuid) + end + end + + context 'with different lease than the active one' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid) + end + + it 'returns silently' do + expect(subject).not_to receive(:command) + expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original + expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original + + subject.perform(project.id, :gc, lease_key, lease_uuid) + end + end + + context 'with no active lease' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(false) + end + + context 'when is able to get the lease' do + before do + allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid) + end + + it "flushes ref caches when the task if 'gc'" do + expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) + + if gitaly + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) + .and_return(nil) + else + expect(Gitlab::Popen).to receive(:popen) + .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + end + + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + + subject.perform(project.id) + end + end + + context 'when no lease can be obtained' do + before do + expect(subject).to receive(:try_obtain_lease).and_return(false) + end + + it 'returns silently' do + expect(subject).not_to receive(:command) + expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original + expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original + + subject.perform(project.id) + end + end end end @@ -39,25 +111,34 @@ describe GitGarbageCollectWorker do end context "repack_full" do + before do + expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + it "calls Gitaly" do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full) .and_return(nil) - subject.perform(project.id, :full_repack) + subject.perform(project.id, :full_repack, lease_key, lease_uuid) end end context "repack_incremental" do + before do + expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + it "calls Gitaly" do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental) .and_return(nil) - subject.perform(project.id, :incremental_repack) + subject.perform(project.id, :incremental_repack, lease_key, lease_uuid) end end shared_examples 'gc tasks' do before do + allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) end @@ -67,7 +148,7 @@ describe GitGarbageCollectWorker do expect(before_packs.count).to be >= 1 - subject.perform(project.id, 'incremental_repack') + subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid) after_packs = packs(project) # Exactly one new pack should have been created @@ -79,12 +160,12 @@ describe GitGarbageCollectWorker do it 'full repack consolidates into 1 packfile' do create_objects(project) - subject.perform(project.id, 'incremental_repack') + subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid) before_packs = packs(project) expect(before_packs.count).to be >= 2 - subject.perform(project.id, 'full_repack') + subject.perform(project.id, 'full_repack', lease_key, lease_uuid) after_packs = packs(project) expect(after_packs.count).to eq(1) @@ -102,7 +183,7 @@ describe GitGarbageCollectWorker do expect(before_packs.count).to be >= 1 - subject.perform(project.id, 'gc') + subject.perform(project.id, 'gc', lease_key, lease_uuid) after_packed_refs = packed_refs(project) after_packs = packs(project) diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz Binary files differindex 92b9860fbc0..561b1e5902c 100644 --- a/vendor/project_templates/rails.tar.gz +++ b/vendor/project_templates/rails.tar.gz |