Commit ec3044ac authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into rc/ce-to-ee-wednesday

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents a79fccf0 cecdaaca
Please view this file on the master branch, on stable branches it's out of date.
## 9.0.4 (2017-04-05)
- No changes.
## 9.0.3 (2017-04-05)
- Allow to edit pipelines quota for user.
......@@ -60,6 +64,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- [Elasticsearch] More efficient search.
- Get Geo secondaries nodes statuses over AJAX.
## 8.17.5 (2017-04-05)
- No changes.
## 8.17.4 (2017-03-19)
- Elastic security fix: Respect feature visibility level.
......@@ -98,6 +106,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Reduce queries needed to check if node is a primary or secondary Geo node.
- Allow squashing merge requests into a single commit.
## 8.16.9 (2017-04-05)
- No changes.
## 8.16.8 (2017-03-19)
- No changes.
......
......@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.0.4 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 9.0.3 (2017-04-05)
- Fix name colision when importing GitHub pull requests from forked repositories. !9719
......@@ -320,6 +328,14 @@ entry.
- Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids.
## 8.17.5 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.17.4 (2017-03-19)
- Only show public emails in atom feeds.
......@@ -534,6 +550,14 @@ entry.
- Remove deprecated GitlabCiService.
- Requeue pending deletion projects.
## 8.16.9 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds.
......
This diff is collapsed.
import Cookies from 'js-cookie';
import BurndownChart from './burndown_chart';
$(() => {
// handle hint dismissal
const hint = $('.burndown-hint');
hint.on('click', '.dismiss-icon', () => {
hint.hide();
Cookies.set('hide_burndown_message', 'true');
});
// generate burndown chart (if data available)
const container = '.burndown-chart';
const $chartElm = $(container);
if ($chartElm.length) {
const startDate = $chartElm.data('startDate');
const dueDate = $chartElm.data('dueDate');
const chartData = $chartElm.data('chartData');
const openIssuesCount = chartData.map(d => [d[0], d[1]]);
const openIssuesWeight = chartData.map(d => [d[0], d[2]]);
const chart = new BurndownChart({ container, startDate, dueDate });
let currentView = 'count';
chart.setData(openIssuesCount, { label: 'Open issues', animate: true });
$('.js-burndown-data-selector').on('click', 'button', function switchData() {
const $this = $(this);
const show = $this.data('show');
if (currentView !== show) {
currentView = show;
$this.addClass('active').siblings().removeClass('active');
switch (show) {
case 'count':
chart.setData(openIssuesCount, { label: 'Open issues', animate: true });
break;
case 'weight':
chart.setData(openIssuesWeight, { label: 'Open issue weight', animate: true });
break;
default:
break;
}
}
});
window.addEventListener('resize', () => chart.animateResize(1));
$(document).on('click', '.js-sidebar-toggle', () => chart.animateResize(2));
}
});
......@@ -163,7 +163,8 @@ export default {
<template v-for="instance in deployBoardData.instances">
<instance-component
:status="instance.status"
:tooltipText="instance.tooltip"/>
:tooltip-text="instance.tooltip"
:stable="instance.stable" />
</template>
</div>
</section>
......
......@@ -7,6 +7,9 @@
* see more information about this in
* https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png
*
* An instance can represent a normal deploy or a canary deploy. In the latter we need to provide
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/
export default {
......@@ -28,11 +31,23 @@ export default {
required: false,
default: '',
},
stable: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
cssClass() {
return `deploy-board-instance-${this.status}`;
let cssClassName = `deploy-board-instance-${this.status}`;
if (!this.stable) {
cssClassName = `${cssClassName} deploy-board-instance-canary`;
}
return cssClassName;
},
},
......
......@@ -240,6 +240,9 @@
border-width: 1px;
border-style: solid;
margin: 1px;
display: flex;
justify-content: center;
align-items: center;
&-finished {
background-color: $green-100;
......@@ -270,6 +273,17 @@
background-color: $white-light;
border-color: $border-color;
}
&.deploy-board-instance-canary {
&::after {
width: 7px;
height: 7px;
border: 1px solid $white-light;
background-color: $orange-300;
border-radius: 50%;
content: "";
}
}
}
.deploy-board-icon i {
......
......@@ -200,3 +200,157 @@
cursor: -webkit-grab;
cursor: grab;
}
// EE-only
.burndown-hint.container-fluid {
border: 1px solid $border-color;
border-radius: $border-radius-default;
position: relative;
margin: $gl-padding 0;
overflow: hidden;
padding-top: 15px;
padding-bottom: 15px;
.dismiss-icon {
position: absolute;
right: $gl-padding;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
z-index: 1;
}
.svg-container {
text-align: center;
svg {
max-width: 200px;
max-height: 200px;
}
}
.inner-content {
@media (max-width: $screen-xs-max) {
padding: 0 28px;
text-align: center;
}
h4 {
color: $gl-text-color;
font-size: 17px;
}
p {
color: $cycle-analytics-box-text-color;
margin-bottom: $gl-padding;
}
}
}
.burndown-header {
margin: 24px 0 12px;
h3 {
font-size: 16px;
margin: 0;
}
.btn-group {
margin-left: 20px;
margin-bottom: 2px;
}
.btn {
font-size: 12px;
@include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
&.active {
background-color: $blue-500;
border-color: $blue-600;
color: $white-light;
}
}
}
.burndown-chart {
width: 100%;
height: 380px;
margin: 5px 0;
@media (max-width: $screen-sm-max) {
height: 320px;
}
@media (max-width: $screen-xs-max) {
height: 200px;
}
.axis {
font-size: 12px;
line,
path {
fill: none;
stroke: $stat-graph-axis-fill;
shape-rendering: crispEdges;
}
}
.axis-label {
text {
fill: $gl-text-color-secondary;
}
line {
stroke: $border-color;
}
}
.legend {
shape-rendering: crispEdges;
text {
font-size: 13px;
fill: $gl-text-color-disabled;
}
rect {
stroke: $border-color;
fill: none;
}
}
.line {
stroke-width: 2px;
fill: none;
&.actual {
stroke: $gl-success;
}
&.ideal {
stroke: $stat-graph-axis-fill;
stroke-dasharray: 6px 6px;
}
}
.focus {
circle {
fill: $white-light;
stroke: $gl-success;
stroke-width: 2px;
}
}
.chart-tooltip {
text {
font-size: 12px;
fill: $white-light;
}
rect {
fill: $black;
}
}
}
class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_read_deploy_board!, only: :status
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
......
......@@ -77,11 +77,6 @@ class Projects::IssuesController < Projects::ApplicationController
@merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
@discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
# Set Issue description based on project template
if @project.issues_template.present?
@issue.description = @project.issues_template
end
respond_with(@issue)
end
......
......@@ -42,6 +42,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
@burndown = Burndown.new(@milestone)
end
def create
......
class Burndown
attr_accessor :start_date, :due_date, :end_date, :issues_count, :issues_weight
def initialize(milestone)
@milestone = milestone
@start_date = @milestone.start_date
@due_date = @milestone.due_date
@end_date = @milestone.due_date
@end_date = Date.today if @end_date.present? && @end_date > Date.today
@issues_count, @issues_weight = milestone.issues.reorder(nil).pluck('COUNT(*), COALESCE(SUM(weight), 0)').first
end
# Returns the chart data in the following format:
# [date, issue count, issue weight] eg: [["2017-03-01", 33, 127], ["2017-03-02", 35, 73], ["2017-03-03", 28, 50]...]
def as_json(opts = nil)
return [] unless valid?
open_issues_count = issues_count
open_issues_weight = issues_weight
start_date.upto(end_date).each_with_object([]) do |date, chart_data|
closed, reopened = closed_and_reopened_issues_by(date)
closed_issues_count = closed.count
closed_issues_weight = sum_issues_weight(closed)
open_issues_count -= closed_issues_count
open_issues_weight -= closed_issues_weight
chart_data << [date.strftime("%Y-%m-%d"), open_issues_count, open_issues_weight]
reopened_count = reopened.count
reopened_weight = sum_issues_weight(reopened)
open_issues_count += reopened_count
open_issues_weight += reopened_weight
end
end
def valid?
start_date && due_date
end
private
def sum_issues_weight(issues)
issues.map(&:weight).compact.sum
end
def closed_and_reopened_issues_by(date)
current_date = date.to_date
closed = issues_with_closed_at.select { |issue| issue.closed_at.to_date == current_date }
reopened = closed.select { |issue| issue.state == 'reopened' }
[closed, reopened]
end
def issues_with_closed_at
@issues_with_closed_at ||=
@milestone.issues.select('closed_at, weight, state').
where('closed_at IS NOT NULL').
order('closed_at ASC')
end
end
......@@ -15,4 +15,19 @@ class MockDeploymentService < DeploymentService
def terminals(environment)
[]
end
def rollout_status(environment)
OpenStruct.new(
instances: rollout_status_instances,
completion: 80,
valid?: true,
complete?: true
)
end
private
def rollout_status_instances
JSON.parse(Rails.root.join('spec', 'fixtures', 'rollout_status_instances.json'))
end
end
......@@ -12,6 +12,6 @@ class MockMonitoringService < MonitoringService
end
def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
JSON.parse(Rails.root.join('spec', 'fixtures', 'metrics.json'))
end
end
......@@ -73,6 +73,10 @@ class ProjectPolicy < BasePolicy
can! :read_environment
can! :read_deployment
can! :read_merge_request
if License.current&.add_on?('GitLab_DeployBoard')
can! :read_deploy_board
end
end
# Permissions given when an user is team member of a project
......
......@@ -39,11 +39,12 @@ class EnvironmentEntity < Grape::Entity
end
expose :rollout_status_path, if: ->(environment, _) { environment.deployment_service_ready? } do |environment|
status_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment,
format: :json)
can?(request.user, :read_deploy_board, environment.project) &&
status_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment,
format: :json)
end
expose :created_at, :updated_at
......
......@@ -7,6 +7,10 @@ module Issues
@issue = project.issues.new(issue_params)
end
def issue_params_from_template
{ description: project.issues_template }
end
def issue_params_with_info_from_discussions
return {} unless merge_request_to_resolve_discussions_of
......@@ -49,8 +53,13 @@ module Issues
[discussion_info, quote].join("\n\n")
end
# Issue params can be built from 3 types of passed params,
# They take precedence over eachother like this
# passed params > discussion params > template params
def issue_params
@issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
@issue_params ||= issue_params_from_template.
merge(issue_params_with_info_from_discussions).
merge(whitelisted_issue_params)
end
def whitelisted_issue_params
......
......@@ -46,6 +46,8 @@
= preserve do
= markdown_field(@milestone, :description)
= render 'shared/milestones/burndown', milestone: @milestone, project: @project, burndown: @burndown
- if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span Assign some issues to this milestone.
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 158"><g fill="none" fill-rule="evenodd" transform="translate(-48-26)"><path fill="#fff" d="m25 28h240v158h-240z"/><g transform="translate(56 37)"><g transform="translate(0 21)"><path fill="#eee" d="m13.591 92.21l3.03-3.979c.669-.879.499-2.134-.38-2.803-.879-.669-2.134-.499-2.803.38l-3.03 3.979c-.669.879-.499 2.134.38 2.803.879.669 2.134.499 2.803-.38m10.902-14.323l3.03-3.979c.669-.879.499-2.134-.38-2.803-.879-.669-2.134-.499-2.803.38l-3.03 3.979c-.669.879-.499 2.134.38 2.803.879.669 2.134.499 2.803-.38m10.902-14.323l3.03-3.979c.669-.879.499-2.134-.38-2.803-.879-.669-2.134-.499-2.803.38l-3.03 3.979c-.669.879-.499 2.134.38 2.803.879.669 2.134.499 2.803-.38m10.902-14.323l3.03-3.979c.669-.879.499-2.134-.38-2.803-.879-.669-2.134-.499-2.803.38l-3.03 3.979c-.669.879-.499 2.134.38 2.803.879.669 2.134.499 2.803-.38m10.902-14.323l3.03-3.979c.669-.879.499-2.134-.38-2.803-.879-.669-2.134-.499-2.803.38l-3.03 3.979c-.669.879-.499 2.134.38 2.803.879.669 2.134.499 2.803-.38m7.167-4.942l2.644 4.244c.584.938 1.818 1.224 2.755.64.938-.584 1.224-1.818.64-2.755l-2.644-4.244c-.584-.938-1.818-1.224-2.755-.64-.938.584-1.224 1.818-.64 2.755m9.517 15.278l2.644 4.244c.584.938 1.818 1.224 2.755.64.938-.584 1.224-1.818.64-2.755l-2.644-4.244c-.584-.938-1.818-1.224-2.755-.64-.938.584-1.224 1.818-.64 2.755m9.517 15.278l2.644 4.244c.584.938 1.818 1.224 2.755.64.938-.584 1.224-1.818.64-2.755l-2.644-4.244c-.584-.938-1.818-1.224-2.755-.64-.938.584-1.224 1.818-.64 2.755m9.517 15.278l2.644 4.244c.584.938 1.818 1.224 2.755.64.938-.584 1.224-1.818.64-2.755l-2.644-4.244c-.584-.938-1.818-1.224-2.755-.64-.938.584-1.224 1.818-.64 2.755m13.423 5.929l3.213-3.831c.71-.846.599-2.108-.247-2.818-.846-.71-2.108-.599-2.818.247l-3.213 3.831c-.71.846-.599 2.108.247 2.818.846.71 2.108.599 2.818-.247m11.567-13.791l3.213-3.831c.71-.846.599-2.108-.247-2.818-.846-.71-2.108-.599-2.818.247l-3.213 3.831c-.71.846-.599 2.108.247 2.818.846.71 2.108.599 2.818-.247m8.253-11.614l2.794 4.146c.617.916 1.86 1.158 2.776.541.916-.617 1.158-1.86.541-2.776l-2.794-4.146c-.617-.916-1.86-1.158-2.776-.541-.916.617-1.158 1.86-.541 2.776m10.06 14.927l2.794 4.146c.617.916 1.86 1.158 2.776.541.916-.617 1.158-1.86.541-2.776l-2.794-4.146c-.617-.916-1.86-1.158-2.776-.541-.916.617-1.158 1.86-.541 2.776m10.06 14.927l2.794 4.146c.617.916 1.86 1.158 2.776.541.916-.617 1.158-1.86.541-2.776l-2.794-4.146c-.617-.916-1.86-1.158-2.776-.541-.916.617-1.158 1.86-.541 2.776m10.06 14.927c.618.917 1.861 1.159 2.777.541.916-.617 1.158-1.86.541-2.776-.618-.917-1.861-1.159-2.777-.541-.916.617-1.158 1.86-.541 2.776"/><g transform="translate(61)"><rect width="3" height="24" fill="#fde5d8" rx="1.5"/><path fill="#fc6d26" d="m3 13v-11l11.533 3.105c1.387.373 1.478 1.207.192 1.868l-11.724 6.03"/></g><path fill="#b5a7dd" d="m166.93 83.6l-13.994-7.365c-.287-.151-.607-.23-.931-.23h-20.485c-.491-.271-.816-.45-4.08-2.251l-24.469-13.5c-.694-.383-1.548-.32-2.178.16l-19.433 14.806-20.783-26.451c-.65-.827-1.83-1.01-2.699-.417l-20.604 14.05-20.904-21.708c-.614 1.222-1.633 2.206-2.881 2.775l22.08 22.924c.677.703 1.761.815 2.567.265l20.456-13.947 20.845 26.53c.675.859 1.915 1.018 2.785.355l19.963-15.21c-.005-.003.678.374 3.39 1.87l24.469 13.5c.296.163.628.249.966.249h20.506l13.556 7.135c.201-1.391.879-2.628 1.864-3.539"/><path fill="#6b4fbb" d="m171 96c4.418 0 8-3.582 8-8 0-4.418-3.582-8-8-8-4.418 0-8 3.582-8 8 0 4.418 3.582 8 8 8m0-4c-2.209 0-4-1.791-4-4 0-2.209 1.791-4 4-4 2.209 0 4 1.791 4 4 0 2.209-1.791 4-4 4m-160-46c4.418 0 8-3.582 8-8 0-4.418-3.582-8-8-8-4.418 0-8 3.582-8 8 0 4.418 3.582 8 8 8m0-4c-2.209 0-4-1.791-4-4 0-2.209 1.791-4 4-4 2.209 0 4 1.791 4 4 0 2.209-1.791 4-4 4"/></g><path fill="#fde5d8" d="m168.78 39.803l-2.908.646c-.542.12-.882-.228-.763-.763l.646-2.908-.646-2.908c-.12-.542.228-.882.763-.763l2.908.646 2.908-.646c.542-.12.882.228.763.763l-.646 2.908.646 2.908c.12.542-.228.882-.763.763l-2.908-.646" transform="matrix(.70711.70711-.70711.70711 75.44-108.57)"/><path fill="#d4cde8" d="m101.36 53.839l-2.21.491c-.537.119-.874-.226-.756-.756l.491-2.21-.491-2.21c-.119-.537.226-.874.756-.756l2.21.491 2.21-.491c.537-.119.874.226.756.756l-.491 2.21.491 2.21c.119.537-.226.874-.756.756l-2.21-.491" transform="matrix(.70711.70711-.70711.70711 66.01-56.631)"/><g fill="#fde5d8"><path d="m125.36 8.839l-2.21.491c-.537.119-.874-.226-.756-.756l.491-2.21-.491-2.21c-.119-.537.226-.874.756-.756l2.21.491 2.21-.491c.537-.119.874.226.756.756l-.491 2.21.491 2.21c.119.537-.226.874-.756.756l-2.21-.491" transform="matrix(.70711.70711-.70711.70711 41.22-86.78)"/><path d="m18.778 23.803l-2.908.646c-.542.12-.882-.228-.763-.763l.646-2.908-.646-2.908c-.12-.542.228-.882.763-.763l2.908.646 2.908-.646c.542-.12.882.228.763.763l-.646 2.908.646 2.908c.12.542-.228.882-.763.763l-2.908-.646" transform="matrix(.70711.70711-.70711.70711 20.19-7.192)"/></g></g></g></svg>
- milestone = local_assigns[:milestone]
- project = local_assigns[:project]
- burndown = local_assigns[:burndown]
- can_generate_chart = burndown&.valid?
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('burndown_chart')
- if can_generate_chart
.burndown-header
%h3
Burndown chart
.btn-group.js-burndown-data-selector
%button.btn.btn-xs.active{ data: { show: 'count' } }
Issues
%button.btn.btn-xs{ data: { show: 'weight' } }
Issue weight
.burndown-chart{ data: { start_date: burndown.start_date.strftime("%Y-%m-%d"), due_date: burndown.due_date.strftime("%Y-%m-%d"), chart_data: burndown.to_json } }
- elsif can?(current_user, :admin_milestone, @project) && cookies['hide_burndown_message'].nil?
.burndown-hint.content-block.container-fluid
= icon("times", class: "dismiss-icon")
.row
.col-sm-4.col-xs-12.svg-container
= custom_icon('icon_burndown_chart_splash')
.col-sm-8.col-xs-12.inner-content
%h4
Burndown chart
%p
View your milestone's progress as a burndown chart. Add both a start and a due date to
this milestone and the chart will appear here, always up-to-date.
= link_to "Add start and due date", edit_namespace_project_milestone_path(project.namespace, project, milestone), class: 'btn'
---
title: Added mock data for Deployboard
merge_request:
author:
---
title: Add burndown chart to milestones
merge_request:
author:
---
title: Add a Rake task to make the current node the primary Geo node
merge_request:
author:
---
title: Visualise Canary Deployments
merge_request:
author:
......@@ -23,6 +23,7 @@ var config = {
main: './main.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
burndown_chart: './burndown_chart/index.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
......@@ -130,6 +131,7 @@ var config = {
'graphs',
'users',
'monitoring',
'burndown_chart',
],
}),
......
require './spec/support/sidekiq'
require './spec/support/test_env'
class Gitlab::Seeder::Burndown
def initialize(project, perf: false)
@project = project
end
def seed!
Timecop.travel 10.days.ago
Sidekiq::Testing.inline! do
create_milestone
puts '.'
create_issues
puts '.'
close_issues
puts '.'
reopen_issues
puts '.'
end
Timecop.return
print '.'
end
private
def create_milestone
milestone_params = {
title: "Sprint - #{FFaker::Lorem.sentence}",
description: FFaker::Lorem.sentence,
state: 'active',
start_date: Date.today,
due_date: rand(5..10).days.from_now
}
@milestone = Milestones::CreateService.new(@project, @project.team.users.sample, milestone_params).execute
end
def create_issues
20.times do
issue_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentence,
state: 'opened',
milestone: @milestone,
assignee: @project.team.users.sample,
weight: rand(1..9)
}
Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
end
end
def close_issues
@milestone.start_date.upto(@milestone.due_date) do |date|
Timecop.travel(date)
close_number = rand(0..2)
open_issues = @milestone.issues.opened
open_issues = open_issues.slice(0..close_number)
open_issues.each do |issue|
Issues::CloseService.new(@project, @project.team.users.sample, {}).execute(issue)
end
end
Timecop.return
end
def reopen_issues
count = @milestone.issues.closed.count / 3
issues = @milestone.issues.closed.slice(0..rand(count))
issues.each { |i| i.update(state: 'reopened') }
end
end
Gitlab::Seeder.quiet do
if project_id = ENV['PROJECT_ID']
project = Project.find(project_id)
seeder = Gitlab::Seeder::Burndown.new(project)
seeder.seed!
else
Project.all.each do |project|
seeder = Gitlab::Seeder::Burndown.new(project)
seeder.seed!
end
end
end
......@@ -101,7 +101,7 @@ Indexing is 65.55% complete (6555/10000 projects)
By default, one job is created for every 300 projects. For large numbers of
projects, you may wish to increase the batch size, by setting the `BATCH`
environment variable. You may also wish to consider [throttling](../administration/operations/sidekiq_job_throttling.md)
the `elastic_batch_project_indexer` queue , as this step can be I/O-intensive.
the `elastic_batch_project_indexer` queue, as this step can be I/O-intensive.
You can also run the initial indexing synchronously - this is most useful if
you have a small number of projects, or need finer-grained control over indexing
......@@ -121,16 +121,16 @@ If you want to run several tasks in parallel (probably in separate terminal
windows) you can provide the `ID_FROM` and `ID_TO` parameters:
```
ID_FROM=1001 ID_TO=2000 sudo gitlab-rake gitlab:elastic:index_repositories
sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000
```
Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional.
As an example, if you have 3,000 repositories and you want to run three separate indexing tasks, you might run:
```
ID_TO=1000 sudo gitlab-rake gitlab:elastic:index_repositories
ID_FROM=1001 ID_TO=2000 sudo gitlab-rake gitlab:elastic:index_repositories
ID_FROM=2001 sudo gitlab-rake gitlab:elastic:index_repositories
sudo gitlab-rake gitlab:elastic:index_repositories ID_TO=1000
sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000
sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=2001
```
Sometimes your repository index process `gitlab:elastic:index_repositories` or
......@@ -144,7 +144,7 @@ it will check every project repository again to make sure that every commit in
that repository is indexed, it can be useful in case if your index is outdated:
```
UPDATE_INDEX=true ID_TO=1000 sudo gitlab-rake gitlab:elastic:index_repositories
sudo gitlab-rake gitlab:elastic:index_repositories UPDATE_INDEX=true ID_TO=1000
```
You can also use the `gitlab:elastic:clear_index_status` Rake task to force the
......
......@@ -10,7 +10,15 @@ module Gitlab
end
def labels
metadata['labels']
metadata.fetch('labels', {})
end
def track
labels.fetch('track', 'stable')
end
def stable?
track == 'stable'
end
def outdated?
......@@ -52,7 +60,12 @@ module Gitlab
end
def deployment_instance(n, name, status)
{ status: status, tooltip: "#{name} (pod #{n}) #{status.capitalize}" }
{
status: status,
tooltip: "#{name} (pod #{n}) #{status.capitalize}",
track: track,
stable: stable?
}
end
def metadata
......
......@@ -72,4 +72,39 @@ namespace :geo do
Rails.application.config = @previous_config[:config]
end
end
desc 'Make this node the Geo primary'
task :set_primary_node, [:ssh_key_filename] => :environment do |_, args|
filename = args[:ssh_key_filename]
abort 'GitLab Geo is not supported with this license. Please contact sales@gitlab.com.' unless Gitlab::Geo.license_allows?
abort 'You must specify a filename of an SSH public key' unless filename.present?
abort 'GitLab Geo primary node already present' if Gitlab::Geo.primary_node.present?
public_key = load_ssh_public_key(filename)
abort "Invalid SSH public key in #{filename}, aborting" unless public_key
set_primary_geo_node(public_key)
end
def load_ssh_public_key(filename)
File.open(filename).read
rescue => e
puts "Error opening #{filename}: #{e}".color(:red)
nil
end
def set_primary_geo_node(public_key)
params = { host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
relative_url_root: Gitlab.config.gitlab.relative_url_root,
primary: true,
geo_node_key_attributes: { key: public_key } }
node = GeoNode.new(params)
puts "Saving primary GeoNode with URL #{node.url}".color(:green)
node.save
puts "Error saving GeoNode:\n#{node.errors.full_messages.join("\n")}".color(:red) unless node.persisted?
end
end
......@@ -11,6 +11,8 @@ describe Projects::EnvironmentsController do
end
before do
allow_any_instance_of(License).to receive(:add_on?).and_return(false)
project.team << [user, :master]
sign_in(user)
......@@ -27,6 +29,8 @@ describe Projects::EnvironmentsController do
context 'when requesting JSON response for folders' do
before do
allow_any_instance_of(Environment).to receive(:deployment_service_ready?).and_return(true)
create(:environment, project: project,
name: 'staging/review-1',
state: :available)
......@@ -44,15 +48,19 @@ describe Projects::EnvironmentsController do
context 'when requesting available environments scope' do
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_DeployBoard').and_return(true)
get :index, environment_params(format: :json, scope: :available)
end
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.first['latest']['rollout_status_path']).to be_present
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
expect(environments.second['latest']['rollout_status_path']).to be_present
end
it 'contains values describing environment scopes sizes' do
......@@ -78,6 +86,19 @@ describe Projects::EnvironmentsController do
expect(json_response['stopped_count']).to eq 1
end
end
context 'when license does not has the GitLab_DeployBoard add-on' do
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_DeployBoard').and_return(false)
get :index, environment_params(format: :json)
end
it 'does not return the rollout_status_path attribute' do
expect(environments.first['latest']['rollout_status_path']).to be_blank
expect(environments.second['latest']['rollout_status_path']).to be_blank
end
end
end
end
......@@ -233,6 +254,7 @@ describe Projects::EnvironmentsController do
let(:project) { create(:kubernetes_project) }
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_DeployBoard').and_return(true)
allow_any_instance_of(Environment).to receive(:deployment_service_ready?).and_return(true)
end
......@@ -256,6 +278,18 @@ describe Projects::EnvironmentsController do
expect(response.status).to eq(200)
end
end
context 'when license does not has the GitLab_DeployBoard add-on' do
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_DeployBoard').and_return(false)
end
it 'does not return any data' do
get :status, environment_params
expect(response).to have_http_status(:not_found)
end
end
end
describe 'GET #metrics' do
......
[
{
"status": "finished",
"tooltip": "production (pod 0) Finished",
"track": "stable",
"stable": true
},
{
"status": "deploying",
"tooltip": "production (pod 1) Deploying",
"track": "stable",
"stable": true
},
{
"status": "failed",
"tooltip": "production (pod 2) Failed",
"track": "stable",
"stable": true
},
{
"status": "ready",
"tooltip": "production (pod 3) Ready",
"track": "stable",
"stable": true
},
{
"status": "preparing",
"tooltip": "production (pod 4) Preparing",
"track": "stable",
"stable": true
},
{
"status": "waiting",
"tooltip": "production (pod 5) Waiting",
"track": "stable",
"stable": true
},
{
"status": "finished",
"tooltip": "production-canary (pod 0) Finished",
"track": "canary",
"stable": false
},
{
"status": "deploying",
"tooltip": "production-canary (pod 1) Deploying",
"track": "canary",
"stable": false
},
{
"status": "failed",
"tooltip": "production-canary (pod 2) Failed",
"track": "canary",
"stable": false
},
{
"status": "ready",
"tooltip": "production-canary (pod 3) Ready",
"track": "canary",
"stable": false
},
{
"status": "preparing",
"tooltip": "production-canary (pod 4) Preparing",
"track": "canary",
"stable": false
},
{
"status": "waiting",
"tooltip": "production-canary (pod 5) Waiting",
"track": "canary",
"stable": false
}
]
......@@ -30,4 +30,15 @@ describe('Deploy Board Instance', () => {
expect(component.$el.classList.contains('deploy-board-instance-deploying')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('');
});
it('should render a div with canary class when stable prop is provided as false', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
stable: false,
},
}).$mount();
expect(component.$el.classList.contains('deploy-board-instance-canary')).toBe(true);
});
});
......@@ -5,60 +5,70 @@ describe Gitlab::Kubernetes::Deployment do
describe '#name' do
let(:params) { named(:selected) }
it { expect(deployment.name).to eq(:selected) }
end
describe '#labels' do
let(:params) { make('metadata', 'labels' => :selected) }
it { expect(deployment.labels).to eq(:selected) }
end
describe '#outdated?' do
context 'when outdated' do
let(:params) { generation(2, 1) }
it { expect(deployment.outdated?).to be_truthy }
end
context 'when up to date' do
let(:params) { generation(2, 2) }
it { expect(deployment.outdated?).to be_falsy }
end
context 'when ahead of latest' do
let(:params) { generation(1, 2) }
it { expect(deployment.outdated?).to be_falsy }
end
end
describe '#wanted_replicas' do
let(:params) { make('spec', 'replicas' => :selected ) }
it { expect(deployment.wanted_replicas).to eq(:selected) }
end
describe '#finished_replicas' do
let(:params) { make('status', 'availableReplicas' => :selected) }
it { expect(deployment.finished_replicas).to eq(:selected) }
end
describe '#deploying_replicas' do
let(:params) { make('status', 'availableReplicas' => 2, 'updatedReplicas' => 4) }
it { expect(deployment.deploying_replicas).to eq(2) }
end
describe '#waiting_replicas' do
let(:params) { combine(make('spec', 'replicas' => 4), make('status', 'updatedReplicas' => 2)) }
it { expect(deployment.waiting_replicas).to eq(2) }
end
describe '#instances' do
context 'when unnamed' do
let(:params) { combine(generation(1, 1), instances) }
it 'returns all instances as unknown and waiting' do
expected = [
{ status: 'waiting', tooltip: 'unknown (pod 0) Waiting' },
{ status: 'waiting', tooltip: 'unknown (pod 1) Waiting' },
{ status: 'waiting', tooltip: 'unknown (pod 2) Waiting' },
{ status: 'waiting', tooltip: 'unknown (pod 3) Waiting' },
{ status: 'waiting', tooltip: 'unknown (pod 0) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'unknown (pod 1) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'unknown (pod 2) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'unknown (pod 3) Waiting', track: 'stable', stable: true },
]
expect(deployment.instances).to eq(expected)
......@@ -67,12 +77,13 @@ describe Gitlab::Kubernetes::Deployment do
context 'when outdated' do
let(:params) { combine(named('foo'), generation(1, 0), instances) }
it 'returns all instances as named and waiting' do
expected = [
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting' },
{ status: 'waiting', tooltip: 'foo (pod 1) Waiting' },
{ status: 'waiting', tooltip: 'foo (pod 2) Waiting' },
{ status: 'waiting', tooltip: 'foo (pod 3) Waiting' },
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 1) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 2) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 3) Waiting', track: 'stable', stable: true },
]
expect(deployment.instances).to eq(expected)
......@@ -84,15 +95,44 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all instances' do
expected = [
{ status: 'finished', tooltip: 'foo (pod 0) Finished' },
{ status: 'deploying', tooltip: 'foo (pod 1) Deploying' },
{ status: 'waiting', tooltip: 'foo (pod 2) Waiting' },
{ status: 'waiting', tooltip: 'foo (pod 3) Waiting' },
{ status: 'finished', tooltip: 'foo (pod 0) Finished', track: 'stable', stable: true },
{ status: 'deploying', tooltip: 'foo (pod 1) Deploying', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 2) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 3) Waiting', track: 'stable', stable: true },
]
expect(deployment.instances).to eq(expected)
end
end
context 'with track label' do
let(:labels) { { 'track' => track } }
let(:params) { combine(named('foo', labels), generation(1, 0), instances(1, 1, 1, labels)) }
context 'when marked as stable' do
let(:track) { 'stable' }
it 'returns all instances' do
expected = [
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting', track: 'stable', stable: true },
]
expect(deployment.instances).to eq(expected)
end
end
context 'when marked as canary' do
let(:track) { 'canary' }
it 'returns all instances' do
expected = [
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting', track: 'canary', stable: false },
]
expect(deployment.instances).to eq(expected)
end
end
end
end
def generation(expected, observed)
......@@ -102,14 +142,14 @@ describe Gitlab::Kubernetes::Deployment do
)
end
def named(name = "foo")
make('metadata', 'name' => name)
def named(name = "foo", labels = {})
make('metadata', 'name' => name, 'labels' => labels)
end
def instances
def instances(replicas = 4, available = 1, updated = 2, labels = {})
combine(
make('spec', 'replicas' => 4),
make('status', 'availableReplicas' => 1, 'updatedReplicas' => 2),
make('spec', 'replicas' => replicas),
make('status', 'availableReplicas' => available, 'updatedReplicas' => updated),
)
end
......
......@@ -2,16 +2,25 @@ require 'spec_helper'
describe Gitlab::Kubernetes::RolloutStatus do
include KubernetesHelpers
let(:specs_all_finished) { [kube_deployment(name: 'one'), kube_deployment(name: 'two')] }
let(:specs_half_finished) do
let(:track) { nil }
let(:specs) { specs_all_finished }
let(:specs_none) { [] }
let(:specs_all_finished) do
[
kube_deployment(name: 'one'),
kube_deployment(name: 'two').deep_merge('status' => { 'availableReplicas' => 0 })
kube_deployment(name: 'two', track: track)
]
end
let(:specs) { specs_all_finished }
let(:specs_none) { [] }
let(:specs_half_finished) do
[
kube_deployment(name: 'one'),
kube_deployment(name: 'two', track: track)
.deep_merge('status' => { 'availableReplicas' => 0 })
]
end
subject(:rollout_status) { described_class.from_specs(*specs) }
......@@ -24,17 +33,36 @@ describe Gitlab::Kubernetes::RolloutStatus do
end
describe '#instances' do
it 'stores the union of deployment instances' do
expected = [
{ status: 'finished', tooltip: 'one (pod 0) Finished' },
{ status: 'finished', tooltip: 'one (pod 1) Finished' },
{ status: 'finished', tooltip: 'one (pod 2) Finished' },
{ status: 'finished', tooltip: 'two (pod 0) Finished' },
{ status: 'finished', tooltip: 'two (pod 1) Finished' },
{ status: 'finished', tooltip: 'two (pod 2) Finished' },
]
expect(rollout_status.instances).to eq(expected)
context 'for stable track' do
it 'stores the union of deployment instances' do
expected = [
{ status: 'finished', tooltip: 'one (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 2) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 2) Finished', track: 'stable', stable: true },
]
expect(rollout_status.instances).to eq(expected)
end
end
context 'for stable track' do
let(:track) { 'canary' }
it 'stores the union of deployment instances' do
expected = [
{ status: 'finished', tooltip: 'one (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 2) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 0) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'two (pod 1) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'two (pod 2) Finished', track: 'canary', stable: false },
]
expect(rollout_status.instances).to eq(expected)
end
end
end
......@@ -47,6 +75,7 @@ describe Gitlab::Kubernetes::RolloutStatus do
context 'when half of the instances are finished' do
let(:specs) { specs_half_finished }
it { is_expected.to eq(50) }
end
end
......@@ -60,6 +89,7 @@ describe Gitlab::Kubernetes::RolloutStatus do
context 'when half of the instances are finished' do
let(:specs) { specs_half_finished }
it { is_expected.to be_falsy}
end
end
......
require 'spec_helper'
describe Burndown, models: true do
let(:start_date) { "2017-03-01" }
let(:due_date) { "2017-03-05" }
let(:milestone) { create(:milestone, start_date: start_date, due_date: due_date) }
let(:project) { milestone.project }
let(:user) { create(:user) }
let(:issue_params) do
{
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentence,
state: 'opened',
milestone: milestone,
weight: 2,
project_id: project.id
}
end
before do
project.add_master(user)
build_sample
end
after do
Timecop.return
end
subject { described_class.new(milestone).to_json }
it "generates an array with date, issue count and weight" do
expect(subject).to eq([
["2017-03-01", 33, 66],
["2017-03-02", 35, 70],
["2017-03-03", 28, 56],
["2017-03-04", 32, 64],
["2017-03-05", 21, 42]
].to_json)
end
it "returns empty array if milestone start date is nil" do
milestone.update(start_date: nil)
expect(subject).to eq([].to_json)
end
it "returns empty array if milestone due date is nil" do
milestone.update(due_date: nil)
expect(subject).to eq([].to_json)
end
it "it counts until today if milestone due date > Date.today" do
Timecop.travel(milestone.due_date - 1.day)
expect(JSON.parse(subject).last[0]).to eq(Time.now.strftime("%Y-%m-%d"))
end
# Creates, closes and reopens issues only for odd days numbers
def build_sample
milestone.start_date.upto(milestone.due_date) do |date|
day = date.day
next if day.even?
count = day * 4
Timecop.travel(date)
# Create issues
issues = create_list(:issue, count, issue_params)
# Close issues
closed = issues.slice(0..count / 2)
closed.each(&:close)
# Reopen issues
closed.slice(0..count / 4).each(&:reopen)
end
Timecop.travel(due_date)
end
end
......@@ -292,7 +292,7 @@ describe 'Git HTTP requests', lib: true do
it 'responds with status 403' do
msg = 'No GitLab Enterprise Edition license has been provided yet. Pushing code and creation of issues and merge requests has been disabled. Ask an admin to upload a license to activate this functionality.'
allow(License).to receive(:current).and_return(false)
allow(License).to receive(:current).and_return(nil)
upload(path, env) do |response|
expect(response).to have_http_status(403)
......
require 'spec_helper'
describe EnvironmentEntity do
let(:user) { create(:user) }
let(:environment) { create(:environment) }
let(:entity) do
described_class.new(environment, request: double(user: nil))
described_class.new(environment, request: double(user: user))
end
let(:environment) { create(:environment) }
subject { entity.as_json }
before do
allow_any_instance_of(License).to receive(:add_on?).and_return(false)
environment.project.team << [user, :master]
end
it 'exposes latest deployment' do
expect(subject).to include(:last_deployment)
end
......@@ -38,6 +46,7 @@ describe EnvironmentEntity do
context 'with deployment service ready' do
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_DeployBoard').and_return(true)
allow(environment).to receive(:deployment_service_ready?).and_return(true)
end
......@@ -47,4 +56,15 @@ describe EnvironmentEntity do
expect(subject[:rollout_status_path]).to eq(expected)
end
end
context 'when license does not has the GitLab_DeployBoard add-on' do
before do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_DeployBoard').and_return(false)
allow(environment).to receive(:deployment_service_ready?).and_return(true)
end
it 'does not expose rollout_status_path' do
expect(subject[:rollout_status_path]).to be_blank
end
end
end
......@@ -8,6 +8,19 @@ describe Issues::BuildService, services: true do
project.team << [user, :developer]
end
context 'with an issue template' do
describe '#execute' do
it 'fills in the template in the description' do
project = build(:project, issues_template: 'Work hard, play hard!')
service = described_class.new(project, user)
issue = service.execute
expect(issue.description).to eq('Work hard, play hard!')
end
end
end
context 'for a single discussion' do
describe '#execute' do
let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) }
......@@ -25,6 +38,16 @@ describe Issues::BuildService, services: true do
expect(issue.description).to include('Almost done')
end
context 'with an issue template' do
let(:project) { create(:project, :repository, issues_template: 'Work hard, play hard!') }
it 'picks the discussion description over the issue template' do
issue = service.execute
expect(issue.description).to include('Almost done')
end
end
end
end
......
......@@ -85,12 +85,15 @@ module KubernetesHelpers
}
end
def kube_deployment(name: "kube-deployment", app: "valid-deployment-label")
def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
{
"metadata" => {
"name" => name,
"generation" => 4,
"labels" => { "app" => app },
"labels" => {
"app" => app,
"track" => track
}.compact,
},
"spec" => { "replicas" => 3 },
"status" => {
......
require 'rake_helper'
describe 'geo rake tasks' do
before do
Rake.application.rake_require 'tasks/geo'
end
describe 'set_primary_node task' do
let(:ssh_key) { 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUkxk8m9rVYZ1q4/5xpg3TwTM9QFw3TinPFkyWsiACFKjor3byV6g3vHWTuIS70E7wk2JTXGL0wdrfUG6iQDJuP0BYNxjkluB14nIAfPuXN7V73QY/cqvHogw5o6pPRFD+Szke6FzouNQ70Z/qrM1k7me3e9DMuscMMrMTOR2HLKppNQyP4Jp0WJOyncdWB2NxKXTezy/ZnHv+BdhC0q0JW3huIx9qkBCHio7x8BdyJLMF9KxNYIuCkbP3exs5wgb+qGrjSri6LfAVq8dJ2VYibWxdsUG6iITJF+G4qbcyQjgiMLbxCfNd9bjwmkxSGvFn2EPsAFKzxyAvYFWb/y91 test@host' }
before do
expect(Gitlab::Geo).to receive(:license_allows?).and_return(true)
end
it 'creates a GeoNode' do
begin
file = Tempfile.new('geo-test-')
file.write(ssh_key)
path = file.path
file.close
expect(GeoNode.count).to eq(0)
run_rake_task('geo:set_primary_node', path)
expect(GeoNode.count).to eq(1)
node = GeoNode.first
expect(node.primary).to be_truthy
expect(node.geo_node_key.key).to eq(ssh_key)
ensure
file.unlink
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment