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.
......
import d3 from 'd3';
const margin = { top: 5, right: 65, bottom: 30, left: 50 };
const parseDate = d3.time.format('%Y-%m-%d').parse;
const bisectDate = d3.bisector(d => d.date).left;
const tooltipPadding = { x: 8, y: 3 };
const tooltipDistance = 15;
export default class BurndownChart {
constructor({ container, startDate, dueDate }) {
this.canvas = d3.select(container).append('svg')
.attr('height', '100%')
.attr('width', '100%');
// create svg nodes
this.chartGroup = this.canvas.append('g').attr('class', 'chart');
this.xAxisGroup = this.chartGroup.append('g').attr('class', 'x axis');
this.yAxisGroup = this.chartGroup.append('g').attr('class', 'y axis');
this.idealLinePath = this.chartGroup.append('path').attr('class', 'ideal line');
this.actualLinePath = this.chartGroup.append('path').attr('class', 'actual line');
this.xAxisGroup.append('line').attr('class', 'domain-line');
// create y-axis label
this.label = 'Remaining';
const yAxisLabel = this.yAxisGroup.append('g').attr('class', 'axis-label');
this.yAxisLabelText = yAxisLabel.append('text').text(this.label);
this.yAxisLabelBBox = this.yAxisLabelText.node().getBBox();
this.yAxisLabelLineA = yAxisLabel.append('line');
this.yAxisLabelLineB = yAxisLabel.append('line');
// create chart legend
this.chartLegendGroup = this.chartGroup.append('g').attr('class', 'legend');
this.chartLegendGroup.append('rect');
this.chartLegendIdealKey = this.chartLegendGroup.append('g');
this.chartLegendIdealKey.append('line').attr('class', 'ideal line');
this.chartLegendIdealKey.append('text').text('Guideline');
this.chartLegendIdealKeyBBox = this.chartLegendIdealKey.select('text').node().getBBox();
this.chartLegendActualKey = this.chartLegendGroup.append('g');
this.chartLegendActualKey.append('line').attr('class', 'actual line');
this.chartLegendActualKey.append('text').text('Progress');
this.chartLegendActualKeyBBox = this.chartLegendActualKey.select('text').node().getBBox();
// create tooltips
this.chartFocus = this.chartGroup.append('g').attr('class', 'focus').style('display', 'none');
this.chartFocus.append('circle').attr('r', 4);
this.tooltipGroup = this.chartFocus.append('g').attr('class', 'chart-tooltip');
this.tooltipGroup.append('rect').attr('rx', 3).attr('ry', 3);
this.tooltipGroup.append('text');
this.chartOverlay = this.chartGroup.append('rect').attr('class', 'overlay')
.on('mouseover', () => this.chartFocus.style('display', null))
.on('mouseout', () => this.chartFocus.style('display', 'none'))
.on('mousemove', () => this.handleMousemove());
// parse start and due dates
this.startDate = parseDate(startDate);
this.dueDate = parseDate(dueDate);
// get width and height
const dimensions = this.canvas.node().getBoundingClientRect();
this.width = dimensions.width;
this.height = dimensions.height;
this.chartWidth = this.width - (margin.left + margin.right);
this.chartHeight = this.height - (margin.top + margin.bottom);
// set default scale domains
this.xMax = this.dueDate;
this.yMax = 1;
// create scales
this.xScale = d3.time.scale()
.range([0, this.chartWidth])
.domain([this.startDate, this.xMax]);
this.yScale = d3.scale.linear()
.range([this.chartHeight, 0])
.domain([0, this.yMax]);
// create axes
this.xAxis = d3.svg.axis()
.scale(this.xScale)
.orient('bottom')
.tickFormat(d3.time.format('%b %-d'))
.tickPadding(6)
.tickSize(4, 0);
this.yAxis = d3.svg.axis()
.scale(this.yScale)
.orient('left')
.tickPadding(6)
.tickSize(4, 0);
// create lines
this.line = d3.svg.line()
.x(d => this.xScale(d.date))
.y(d => this.yScale(d.value));
// render the chart
this.scheduleRender();
}
// set data and force re-render
setData(data, { label = 'Remaining', animate } = {}) {
this.data = data.map(datum => ({
date: parseDate(datum[0]),
value: parseInt(datum[1], 10),
})).sort((a, b) => (a.date - b.date));
// adjust axis domain to correspond with data
this.xMax = Math.max(d3.max(this.data, d => d.date) || 0, this.dueDate);
this.yMax = d3.max(this.data, d => d.value) || 1;
this.xScale.domain([this.startDate, this.xMax]);
this.yScale.domain([0, this.yMax]);
// calculate the bounding box for the axis label if updated
// (this must be done here to prevent layout thrashing)
if (this.label !== label) {
this.label = label;
this.yAxisLabelBBox = this.yAxisLabelText.text(label).node().getBBox();
}
// set ideal line data
if (this.data.length > 1) {
const idealStart = this.data[0] || { date: this.startDate, value: 0 };
const idealEnd = { date: this.dueDate, value: 0 };
this.idealData = [idealStart, idealEnd];
}
this.scheduleLineAnimation = !!animate;
this.scheduleRender();
}
handleMousemove() {
if (!this.data) return;
const mouseOffsetX = d3.mouse(this.chartOverlay.node())[0];
const dateOffset = this.xScale.invert(mouseOffsetX);
const i = bisectDate(this.data, dateOffset, 1);
const d0 = this.data[i - 1];
const d1 = this.data[i];
if (d1 == null || dateOffset - d0.date < d1.date - dateOffset) {
this.renderTooltip(d0);
} else {
this.renderTooltip(d1);
}
}
// reset width and height to match the svg element, then re-render if necessary
handleResize() {
const dimensions = this.canvas.node().getBoundingClientRect();
if (this.width !== dimensions.width || this.height !== dimensions.height) {
this.width = dimensions.width;
this.height = dimensions.height;
// adjust axis range to correspond with chart size
this.chartWidth = this.width - (margin.left + margin.right);
this.chartHeight = this.height - (margin.top + margin.bottom);
this.xScale.range([0, this.chartWidth]);
this.yScale.range([this.chartHeight, 0]);
this.scheduleRender();
}
}
scheduleRender() {
if (this.queuedRender == null) {
this.queuedRender = requestAnimationFrame(() => this.render());
}
}
render() {
this.queuedRender = null;
this.renderedTooltipPoint = null; // force tooltip re-render
this.xAxis.ticks(Math.floor(this.chartWidth / 120));
this.yAxis.ticks(Math.min(Math.floor(this.chartHeight / 60), this.yMax));
this.chartGroup.attr('transform', `translate(${margin.left}, ${margin.top})`);
this.xAxisGroup.attr('transform', `translate(0, ${this.chartHeight})`);
this.xAxisGroup.call(this.xAxis);
this.yAxisGroup.call(this.yAxis);
// replace x-axis line with one which continues into the right margin
this.xAxisGroup.select('.domain').remove();
this.xAxisGroup.select('.domain-line').attr('x1', 0).attr('x2', this.chartWidth + margin.right);
// update y-axis label
const axisLabelOffset = (this.yAxisLabelBBox.height / 2) - margin.left;
const axisLabelPadding = (this.chartHeight - this.yAxisLabelBBox.width - 10) / 2;
this.yAxisLabelText
.attr('y', 0 - margin.left)
.attr('x', 0 - (this.chartHeight / 2))
.attr('dy', '1em')
.style('text-anchor', 'middle')
.attr('transform', 'rotate(-90)');
this.yAxisLabelLineA
.attr('x1', axisLabelOffset)
.attr('x2', axisLabelOffset)
.attr('y1', 0)
.attr('y2', axisLabelPadding);
this.yAxisLabelLineB
.attr('x1', axisLabelOffset)
.attr('x2', axisLabelOffset)
.attr('y1', this.chartHeight - axisLabelPadding)
.attr('y2', this.chartHeight);
// update legend
const legendPadding = 10;
const legendSpacing = 5;
const idealBBox = this.chartLegendIdealKeyBBox;
const actualBBox = this.chartLegendActualKeyBBox;
const keyWidth = Math.ceil(Math.max(idealBBox.width, actualBBox.width));
const keyHeight = Math.ceil(Math.max(idealBBox.height, actualBBox.height));
const idealKeyOffset = legendPadding;
const actualKeyOffset = legendPadding + keyHeight + legendSpacing;
const legendWidth = (legendPadding * 2) + 24 + keyWidth;
const legendHeight = (legendPadding * 2) + (keyHeight * 2) + legendSpacing;
const legendOffset = (this.chartWidth + margin.right) - legendWidth - 1;
this.chartLegendGroup.select('rect')
.attr('width', legendWidth)
.attr('height', legendHeight);
this.chartLegendGroup.selectAll('text')
.attr('x', 24)
.attr('dy', '1em');
this.chartLegendGroup.selectAll('line')
.attr('y1', keyHeight / 2)
.attr('y2', keyHeight / 2)
.attr('x1', 0)
.attr('x2', 18);
this.chartLegendGroup.attr('transform', `translate(${legendOffset}, 0)`);
this.chartLegendIdealKey.attr('transform', `translate(${legendPadding}, ${idealKeyOffset})`);
this.chartLegendActualKey.attr('transform', `translate(${legendPadding}, ${actualKeyOffset})`);
// update overlay
this.chartOverlay
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', this.chartWidth)
.attr('height', this.chartHeight);
// render lines if data available
if (this.data != null && this.data.length > 1) {
this.actualLinePath.datum(this.data).attr('d', this.line);
this.idealLinePath.datum(this.idealData).attr('d', this.line);
if (this.scheduleLineAnimation === true) {
this.scheduleLineAnimation = false;
// hide tooltips until animation is finished
this.chartFocus.attr('opacity', 0);
this.constructor.animateLinePath(this.actualLinePath, 800, () => {
this.chartFocus.attr('opacity', null);
});
}
}
}
renderTooltip(datum) {
if (this.renderedTooltipPoint === datum) return;
this.renderedTooltipPoint = datum;
// generate tooltip content
const format = d3.time.format('%b %-d, %Y');
const tooltip = `${datum.value} ${this.label} / ${format(datum.date)}`;
// move the tooltip point of origin to the point on the graph
const x = this.xScale(datum.date);
const y = this.yScale(datum.value);
const textSize = this.tooltipGroup.select('text').text(tooltip).node().getBBox();
const width = textSize.width + (tooltipPadding.x * 2);
const height = textSize.height + (tooltipPadding.y * 2);
// calculate bounraries
const xMin = 0 - x - margin.left;
const yMin = 0 - y - margin.top;
const xMax = (this.chartWidth + margin.right) - x - width;
const yMax = (this.chartHeight + margin.bottom) - y - height;
// try to fit tooltip above point
let xOffset = 0 - Math.floor(width / 2);
let yOffset = 0 - tooltipDistance - height;
if (yOffset <= yMin) {
// else try to fit tooltip to the right
xOffset = tooltipDistance;
yOffset = 0 - Math.floor(height / 2);
if (xOffset >= xMax) {
// else place tooltip on the left
xOffset = 0 - tooltipDistance - width;
}
}
// ensure coordinates keep the entire tooltip in-bounds
xOffset = Math.max(xMin, Math.min(xMax, xOffset));
yOffset = Math.max(yMin, Math.min(yMax, yOffset));
// move everything into place
this.chartFocus.attr('transform', `translate(${x}, ${y})`);
this.tooltipGroup.attr('transform', `translate(${xOffset}, ${yOffset})`);
this.tooltipGroup.select('text')
.attr('dy', '1em')
.attr('x', tooltipPadding.x)
.attr('y', tooltipPadding.y);
this.tooltipGroup.select('rect')
.attr('width', width)
.attr('height', height);
}
animateResize(seconds = 5) {
this.ticksLeft = this.ticksLeft || 0;
if (this.ticksLeft <= 0) {
const interval = setInterval(() => {
this.ticksLeft -= 1;
if (this.ticksLeft <= 0) {
clearInterval(interval);
}
this.handleResize();
}, 20);
}
this.ticksLeft = seconds * 50;
}
static animateLinePath(path, duration = 1000, cb) {
// hack to run a callback at transition end
function after(transition, callback) {
let i = 0;
transition
.each(() => (i += 1))
.each('end', function end(...args) {
i -= 1;
if (i === 0) {
callback.apply(this, args);
}
});
}
const lineLength = path.node().getTotalLength();
path
.attr('stroke-dasharray', `${lineLength} ${lineLength}`)
.attr('stroke-dashoffset', lineLength)
.transition()
.duration(duration)
.ease('linear')
.attr('stroke-dashoffset', 0)
.call(after, () => {
path.attr('stroke-dasharray', null);
if (cb) cb();
});
}
}
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,6 +39,7 @@ class EnvironmentEntity < Grape::Entity
end
expose :rollout_status_path, if: ->(environment, _) { environment.deployment_service_ready? } do |environment|
can?(request.user, :read_deploy_board, environment.project) &&
status_namespace_project_environment_path(
environment.project.namespace,
environment.project,
......
......@@ -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,20 +33,39 @@ describe Gitlab::Kubernetes::RolloutStatus do
end
describe '#instances' do
context 'for stable track' 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' },
{ 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
describe '#completion' do
subject { rollout_status.completion }
......@@ -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