Commit 14e21931 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-02-07' into 'master'

CE upstream - 2018-02-07 20:29 UTC

Closes #4813, gitaly#880, gitaly#1000, and gitaly#994

See merge request gitlab-org/gitlab-ee!4434
parents e02a6804 e79767a8
......@@ -32,6 +32,7 @@ export default class Clusters {
installIngressPath,
installRunnerPath,
installPrometheusPath,
managePrometheusPath,
clusterStatus,
clusterStatusReason,
helpPath,
......@@ -40,6 +41,7 @@ export default class Clusters {
this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({
......@@ -95,6 +97,7 @@ export default class Clusters {
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
},
});
},
......
......@@ -32,6 +32,10 @@
type: String,
required: false,
},
manageLink: {
type: String,
required: false,
},
description: {
type: String,
required: true,
......@@ -89,6 +93,12 @@
return label;
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_ERROR ||
this.requestStatus === REQUEST_FAILURE;
......@@ -141,9 +151,21 @@
<div v-html="description"></div>
</div>
<div
class="table-section table-button-footer section-15 section-align-top"
class="table-section table-button-footer section-align-top"
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
role="gridcell"
>
<div
v-if="showManageButton"
class="btn-group table-action-buttons"
>
<a
class="btn"
:href="manageLink"
>
{{ manageButtonLabel }}
</a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
class="js-cluster-application-install-button"
......
......@@ -23,13 +23,19 @@
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`)),
{
_.escape(s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
......@@ -96,11 +102,12 @@
},
prometheusDescription() {
return sprintf(
_.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`)),
{
_.escape(s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
)), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
......@@ -149,6 +156,7 @@ target="_blank" rel="noopener noreferrer">
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
......
......@@ -45,6 +45,10 @@ export default class ClusterStore {
this.state.ingressHelpPath = ingressHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
this.state.managePrometheusPath = managePrometheusPath;
}
updateStatus(status) {
this.state.status = status;
}
......
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
......@@ -395,6 +396,26 @@ export const spriteIcon = (icon, className = '') => {
return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
};
/**
* This method takes in object with snake_case property names
* and returns new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*/
export const convertObjectPropsToCamelCase = (obj = {}) => {
if (obj === null) {
return {};
}
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
result[convertToCamelCase(prop)] = obj[prop];
return acc;
}, {});
};
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
window.gl = window.gl || {};
......
......@@ -9,6 +9,20 @@ import {
window.timeago = timeago;
window.dateFormat = dateFormat;
/**
* Returns i18n month names array.
* If `abbreviated` is provided, returns abbreviated
* name.
*
* @param {Boolean} abbreviated
*/
const getMonthNames = (abbreviated) => {
if (abbreviated) {
return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
}
return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
};
/**
* Given a date object returns the day of the week in English
* @param {date} date
......@@ -157,7 +171,7 @@ export function timeIntervalInWords(intervalInSeconds) {
return text;
}
export function dateInWords(date, abbreviated = false) {
export function dateInWords(date, abbreviated = false, hideYear = false) {
if (!date) return date;
const month = date.getMonth();
......@@ -168,9 +182,115 @@ export function dateInWords(date, abbreviated = false) {
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
if (hideYear) {
return `${monthName} ${date.getDate()}`;
}
return `${monthName} ${date.getDate()}, ${year}`;
}
/**
* Returns month name based on provided date.
*
* @param {Date} date
* @param {Boolean} abbreviated
*/
export const monthInWords = (date, abbreviated = false) => {
if (!date) {
return '';
}
return getMonthNames(abbreviated)[date.getMonth()];
};
/**
* Returns number of days in a month for provided date.
* courtesy: https://stacko(verflow.com/a/1185804/414749
*
* @param {Date} date
*/
export const totalDaysInMonth = (date) => {
if (!date) {
return 0;
}
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
};
/**
* Returns list of Dates referring to Sundays of the month
* based on provided date
*
* @param {Date} date
*/
export const getSundays = (date) => {
if (!date) {
return [];
}
const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
const month = date.getMonth();
const year = date.getFullYear();
const sundays = [];
const dateOfMonth = new Date(year, month, 1);
while (dateOfMonth.getMonth() === month) {
const dayName = getDayName(dateOfMonth);
if (dayName === 'Sunday') {
sundays.push(new Date(dateOfMonth.getTime()));
}
const daysUntilNextSunday = daysToSunday.indexOf(dayName) + 1;
dateOfMonth.setDate(dateOfMonth.getDate() + daysUntilNextSunday);
}
return sundays;
};
/**
* Returns list of Dates representing a timeframe of Months from month of provided date (inclusive)
* up to provided length
*
* For eg;
* If current month is January 2018 and `length` provided is `6`
* Then this method will return list of Date objects as follows;
*
* [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ]
*
* If current month is March 2018 and `length` provided is `3`
* Then this method will return list of Date objects as follows;
*
* [ February 2018, March 2018, April 2018 ]
*
* @param {Number} length
* @param {Date} date
*/
export const getTimeframeWindow = (length, date) => {
if (!length) {
return [];
}
const currentDate = date instanceof Date ? date : new Date();
const currentMonthIndex = Math.floor(length / 2);
const timeframe = [];
// Move date object backward to the first month of timeframe
currentDate.setDate(1);
currentDate.setMonth(currentDate.getMonth() - currentMonthIndex);
// Iterate and update date for the size of length
// and push date reference to timeframe list
for (let i = 0; i < length; i += 1) {
timeframe.push(new Date(currentDate.getTime()));
currentDate.setMonth(currentDate.getMonth() + 1);
}
// Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
return timeframe;
};
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -73,3 +73,10 @@ export function capitalizeFirstCharacter(text) {
* @returns {String}
*/
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
/**
* Converts snake_case string to camelCase
*
* @param {*} string
*/
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
......@@ -27,6 +27,7 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
clustersPath: metricsData.clustersPath,
tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics,
......@@ -132,6 +133,7 @@
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
......
......@@ -10,6 +10,11 @@
required: false,
default: '',
},
clustersPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
......@@ -35,7 +40,10 @@
title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Configure Prometheus',
buttonText: 'Install Prometheus on clusters',
buttonPath: this.clustersPath,
secondaryButtonText: 'Configure existing Prometheus',
secondaryButtonPath: this.settingsPath,
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
......@@ -43,6 +51,7 @@
description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyUnableToConnectSvgPath,
......@@ -50,12 +59,14 @@
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath,
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
},
};
......@@ -65,13 +76,6 @@
return this.states[this.selectedState];
},
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
......@@ -99,11 +103,21 @@
</p>
<div class="state-button">
<a
v-if="currentState.buttonPath"
class="btn btn-success"
:href="buttonPath"
:href="currentState.buttonPath"
>
{{ currentState.buttonText }}
</a>
</div>
<div class="state-button">
<a
v-if="currentState.secondaryButtonPath"
class="btn"
:href="currentState.secondaryButtonPath"
>
{{ currentState.secondaryButtonText }}
</a>
</div>
</div>
</template>
/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
import axios from './lib/utils/axios_utils';
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
/**
......@@ -146,23 +147,25 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true;
return $.get(this.autocompletePath, {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
}, (response) => {
var firstCategory, i, lastCategory, len, suggestion;
return axios.get(this.autocompletePath, {
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
},
}).then((response) => {
// Hide dropdown menu if no suggestions returns
if (!response.length) {
if (!response.data.length) {
this.disableAutocomplete();
return;
}
const data = [];
// List results
firstCategory = true;
for (i = 0, len = response.length; i < len; i += 1) {
suggestion = response[i];
let firstCategory = true;
let lastCategory;
for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
......@@ -177,7 +180,7 @@ export default class SearchAutocomplete {
lastCategory = suggestion.category;
}
data.push({
id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
......@@ -187,13 +190,17 @@ export default class SearchAutocomplete {
if (data.length) {
data.push('separator');
data.push({
text: "Result name contains \"" + term + "\"",
url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()),
text: `Result name contains "${term}"`,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
return callback(data);
})
.always(() => { this.loadingSuggestions = false; });
callback(data);
this.loadingSuggestions = false;
}).catch(() => {
this.loadingSuggestions = false;
});
}
getCategoryContents() {
......
......@@ -215,6 +215,7 @@ $tooltip-font-size: 12px;
* Padding
*/
$gl-padding: 16px;
$gl-padding-8: 8px;
$gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px;
......
......@@ -365,7 +365,7 @@
}
.prometheus-state {
max-width: 430px;
max-width: 460px;
margin: 10px auto;
text-align: center;
......@@ -373,6 +373,10 @@
max-width: 80vw;
margin: 0 auto;
}
.state-button {
padding: $gl-padding / 2;
}
}
.environments-actions {
......
......@@ -135,6 +135,17 @@
padding-top: 0;
}
.integration-settings-form {
.well {
padding: $gl-padding / 2;
box-shadow: none;
}
.svg-container {
max-width: 150px;
}
}
.token-token-container {
#impersonation-token-token {
width: 80%;
......
......@@ -16,10 +16,7 @@ module Ci
@builds = @config_processor.builds
@jobs = @config_processor.jobs
end
rescue
@error = 'Undefined error'
@status = false
ensure
render :show
end
end
......
......@@ -33,6 +33,7 @@ module ServiceParams
:issues_events,
:issues_url,
:jira_issue_transition_id,
:manual_configuration,
:merge_requests_events,
:mock_service_url,
:namespace,
......
......@@ -53,10 +53,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
set_pipeline_variables
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37432
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
render
end
format.json do
......
......@@ -28,7 +28,7 @@ class SnippetsFinder < UnionFinder
segments << items.public_to_user(current_user)
segments << authorized_to_user(items) if current_user
find_union(segments, Snippet)
find_union(segments, Snippet.includes(:author))
end
def authorized_to_user(items)
......
module GraphHelper
def get_refs(repo, commit)
refs = ""
# Commit::ref_names already strips the refs/XXX from important refs (e.g. refs/heads/XXX)
# so anything leftover is internally used by GitLab
commit_refs = commit.ref_names(repo).reject { |name| name.starts_with?('refs/') }
refs << commit_refs.join(' ')
def refs(repo, commit)
refs = commit.ref_names(repo).join(' ')
# append note count
notes_count = @graph.notes[commit.id]
......
......@@ -407,7 +407,7 @@ module Ci
@config_processor ||= begin
initialize_yaml_processor
rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
rescue Gitlab::Ci::YamlProcessor::ValidationError => e
self.yaml_errors = e.message
nil
rescue
......
......@@ -10,10 +10,26 @@ module Clusters
default_value_for :version, VERSION
state_machine :status do
after_transition any => [:installed] do |application|
application.cluster.projects.each do |project|
project.find_or_initialize_service('prometheus').update(active: true)
end
end
end
def chart
'stable/prometheus'
end
def service_name
'prometheus-prometheus-server'
end
def service_port
80
end
def chart_values_file
"#{Rails.root}/vendor/#{name}/values.yaml"
end
......@@ -21,6 +37,22 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end
def proxy_client
return unless kube_client
proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE)
# ensures headers containing auth data are appended to original k8s client options
options = kube_client.rest_client.options.merge(headers: kube_client.headers)
RestClient::Resource.new(proxy_url, options)
end
private
def kube_client
cluster&.kubeclient
end
end
end
end
......@@ -51,6 +51,9 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
scope :for_environment, -> (env) { where(environment_scope: ['*', '', env.slug]) }
scope :for_all_environments, -> { where(environment_scope: ['*', '']) }
def status_name
if provider
provider.status_name
......
......@@ -290,7 +290,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def keep_around_commits
[repository, merge_request.source_project.repository].each do |repo|
[repository, merge_request.source_project.repository].uniq.each do |repo|
repo.keep_around(start_commit_sha)
repo.keep_around(head_commit_sha)
repo.keep_around(base_commit_sha)
......
......@@ -7,11 +7,14 @@ class PrometheusService < MonitoringService
# Access to prometheus is directly through the API
prop_accessor :api_url
boolean_accessor :manual_configuration
with_options presence: true, if: :activated? do
with_options presence: true, if: :manual_configuration? do
validates :api_url, url: true
end
before_save :synchronize_service_state!
after_save :clear_reactive_cache!
def initialize_properties
......@@ -20,12 +23,20 @@ class PrometheusService < MonitoringService
end
end
def show_active_box?
false
end
def editable?
manual_configuration? || !prometheus_installed?
end
def title
'Prometheus'
end
def description
s_('PrometheusService|Prometheus monitoring')
s_('PrometheusService|Time-series monitoring service')
end
def self.to_param
......@@ -33,7 +44,15 @@ class PrometheusService < MonitoringService
end
def fields
return [] unless editable?
[
{
type: 'checkbox',
name: 'manual_configuration',
title: s_('PrometheusService|Active'),
required: true
},
{
type: 'text',
name: 'api_url',
......@@ -59,7 +78,7 @@ class PrometheusService < MonitoringService
end
def deployment_metrics(deployment)
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics))
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &method(:rename_data_to_metrics))
metrics&.merge(deployment_time: deployment.created_at.to_i) || {}
end
......@@ -68,7 +87,7 @@ class PrometheusService < MonitoringService
end
def additional_deployment_metrics(deployment)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.id, &:itself)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.environment.id, deployment.id, &:itself)
end
def matched_metrics
......@@ -79,6 +98,9 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete?
environment_id = args.first
client = client(environment_id)
data = Kernel.const_get(query_class_name).new(client).query(*args)
{
success: true,
......@@ -89,14 +111,55 @@ class PrometheusService < MonitoringService
{ success: false, result: err.message }
end
def client
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url)
def client(environment_id = nil)
if manual_configuration?
Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url))
else
cluster = cluster_with_prometheus(environment_id)
raise Gitlab::PrometheusError, "couldn't find cluster with Prometheus installed" unless cluster
rest_client = client_from_cluster(cluster)
raise Gitlab::PrometheusError, "couldn't create proxy Prometheus client" unless rest_client
Gitlab::PrometheusClient.new(rest_client)
end
end
def prometheus_installed?
return false if template?
return false unless project
project.clusters.enabled.any? { |cluster| cluster.application_prometheus&.installed? }
end
private
def cluster_with_prometheus(environment_id = nil)
clusters = if environment_id
::Environment.find_by(id: environment_id).try do |env|
# sort results by descending order based on environment_scope being longer
# thus more closely matching environment slug
project.clusters.enabled.for_environment(env).sort_by { |c| c.environment_scope&.length }.reverse!
end
else
project.clusters.enabled.for_all_environments
end
clusters&.detect { |cluster| cluster.application_prometheus&.installed? }
end
def client_from_cluster(cluster)
cluster.application_prometheus.proxy_client
end
def rename_data_to_metrics(metrics)
metrics[:metrics] = metrics.delete :data
metrics
end
def synchronize_service_state!
self.active = prometheus_installed? || manual_configuration?
true
end
end
......@@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include MarkupHelper
include TreeHelper
include Gitlab::Utils::StrongMemoize
presents :merge_request
......@@ -43,7 +44,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def revert_in_fork_path
if user_can_fork_project? && can_be_reverted?(current_user)
if user_can_fork_project? && cached_can_be_reverted?
continue_params = {
to: merge_request_path(merge_request),
notice: "#{edit_in_new_fork_notice} Try to cherry-pick this commit again.",
......@@ -157,7 +158,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def can_revert_on_current_merge_request?
user_can_collaborate_with_project? && can_be_reverted?(current_user)
user_can_collaborate_with_project? && cached_can_be_reverted?
end
def can_cherry_pick_on_current_merge_request?
......@@ -170,6 +171,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
private
def cached_can_be_reverted?
strong_memoize(:can_be_reverted) do
can_be_reverted?(current_user)
end
end
def conflicts
@conflicts ||= MergeRequests::Conflicts::ListService.new(merge_request)
end
......
......@@ -7,6 +7,8 @@ module Ci
# stage.
#
class EnsureStageService < BaseService
EnsureStageError = Class.new(StandardError)
def execute(build)
@build = build
......@@ -22,8 +24,16 @@ module Ci
private
def ensure_stage
def ensure_stage(attempts: 2)
find_stage || create_stage
rescue ActiveRecord::RecordNotUnique
retry if (attempts -= 1) > 0
raise EnsureStageError, <<~EOS
We failed to find or create a unique pipeline stage after 2 retries.
This should never happen and is most likely the result of a bug in
the database load balancing code.
EOS
end
def find_stage
......
module Ci
class RetryBuildService < ::BaseService
CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
allow_failure stage_id stage stage_idx trigger_request
allow_failure stage stage_id stage_idx trigger_request
yaml_variables when environment coverage_regex
description tag_list protected].freeze
......
......@@ -9,10 +9,7 @@ module MergeRequests
merge_request.source_branch = params[:source_branch]
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37439
Gitlab::GitalyClient.allow_n_plus_1_calls do
create(merge_request)
end
create(merge_request)
end
def before_create(merge_request)
......
.layout-page{ class: page_with_sidebar_class }
- if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper
.content-wrapper{ class: "#{@content_wrapper_class}" }
= render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
......
......@@ -14,7 +14,8 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address') } }
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice
.flash-container
......
......@@ -13,6 +13,7 @@
= link_to @environment.name, environment_path(@environment)
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
......
......@@ -13,7 +13,7 @@
},
time: c.time,
space: c.spaces.first,
refs: get_refs(@graph.repo, c),
refs: refs(@graph.repo, c),
id: c.sha,
date: c.date,
message: c.message,
......
......@@ -9,7 +9,7 @@
%p= @service.description
.col-lg-9
= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
- if @service.editable?
.footer-block.row-content-block
......
%h4
= s_('PrometheusService|Auto configuration')
- if @service.manual_configuration?
.well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
.row
- if @service.prometheus_installed?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
.col-sm-10
%p.text-success.prepend-top-default
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
.col-sm-10
%p.prepend-top-default
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success'
%hr
%h4.append-bottom-default
= s_('PrometheusService|Manual configuration')
- unless @service.editable?
.well
= s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters')
- link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
= image_tag avatar_icon(snippet.author), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
......
---
title: Fix N+1 query problem for snippets dashboard.
merge_request: 16944
author:
type: performance
---
title: 'Handle all Psych YAML parser exceptions (fixes #41209)'
merge_request:
author:
type: fixed
---
title: Remove duplicate calls of MergeRequest#can_be_reverted?
merge_request:
author:
type: performance
---
title: Implement multi server support and use kube proxy to connect to Prometheus
servers inside K8S cluster
merge_request: 16182
author:
type: added
class RemoveRedundantPipelineStages < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up(attempts: 100)
remove_redundant_pipeline_stages!
remove_outdated_index!
add_unique_index!
rescue ActiveRecord::RecordNotUnique
retry if (attempts -= 1) > 0
raise StandardError, <<~EOS
Failed to add an unique index to ci_stages, despite retrying the
migration 100 times.
See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16580.
EOS
end
def down
remove_concurrent_index :ci_stages, [:pipeline_id, :name], unique: true
add_concurrent_index :ci_stages, [:pipeline_id, :name]
end
private
def remove_outdated_index!
return unless index_exists?(:ci_stages, [:pipeline_id, :name])
remove_concurrent_index :ci_stages, [:pipeline_id, :name]
end
def add_unique_index!
add_concurrent_index :ci_stages, [:pipeline_id, :name], unique: true
end
def remove_redundant_pipeline_stages!
disable_statement_timeout
redundant_stages_ids = <<~SQL
SELECT id FROM ci_stages WHERE (pipeline_id, name) IN (
SELECT pipeline_id, name FROM ci_stages
GROUP BY pipeline_id, name HAVING COUNT(*) > 1
)
SQL
execute <<~SQL
UPDATE ci_builds SET stage_id = NULL WHERE stage_id IN (#{redundant_stages_ids})
SQL
if Gitlab::Database.postgresql?
execute <<~SQL
DELETE FROM ci_stages WHERE id IN (#{redundant_stages_ids})
SQL
else # We can't modify a table we are selecting from on MySQL
execute <<~SQL
DELETE a FROM ci_stages AS a, ci_stages AS b
WHERE a.pipeline_id = b.pipeline_id AND a.name = b.name
AND a.id <> b.id
SQL
end
end
end
......@@ -543,7 +543,7 @@ ActiveRecord::Schema.define(version: 20180206200543) do
t.integer "lock_version"
end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", using: :btree
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree
add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
......
......@@ -664,7 +664,6 @@ Example response:
]
```
## Project Search API
Search within the specified project.
......
......@@ -26,7 +26,7 @@ Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performan
This will create a `performance` job in your CI/CD pipeline and will run Sitespeed.io against the webpage you define. The GitLab plugin for Sitespeed.io downloaded in order to export the results to JSON. For further customization options of Sitespeed.io, including the ability to provide a list of URLs to test, please consult their [documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
For [GitLab Premium](https://about.gitlab.com/products/) users, a performance score can be automatically
extracted and shown right in the merge request widget. [Learn more on performance diffs in merge requests](../../user/project/merge_requests/browser_performance_testing.md).
extracted and shown right in the merge request widget. Learn more about [Browser Performance Testing](../../user/project/merge_requests/browser_performance_testing.md).
## Performance testing on Review Apps
......
......@@ -102,6 +102,11 @@ running:
kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
NOTE: **Note:**
If your ingress controller has been installed in a different way, you can find
how to get the external IP address in the
[Cluster documentation](../../user/project/clusters/index.md#getting-the-external-ip-address).
Use this IP address to configure your DNS. This part heavily depends on your
preferences and domain provider. But in case you are not sure, just create an
A record with a wildcard host like `*.<your-domain>`.
......
......@@ -134,6 +134,41 @@ added directly to your configured cluster. Those applications are needed for
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
## Getting the external IP address
NOTE: **Note:**
You need a load balancer installed in your cluster in order to obtain the
external IP address with the following procedure. It can be deployed using the
**Ingress** application described in the previous section.
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
and select the proper project and cluster. Then click on **Connect** and execute
the `gcloud` command in a local terminal or using the **Cloud Shell**.
If the cluster is not on GKE, follow the specific instructions for your
Kubernetes provider to configure `kubectl` with the right credentials.
If you installed the Ingress using the **Applications** section, run the following command:
```bash
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
Otherwise, you can list the IP addresses of all load balancers:
```bash
kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
```
The output is the external IP address of your cluster. This information can then
be used to set up DNS entries and forwarding rules that allow external access to
your deployed applications.
## Setting the environment scope
When adding more than one clusters, you need to differentiate them with an
......
......@@ -34,7 +34,7 @@ and deploy from one single platform. Issue Boards help you to visualize
and manage the entire process _in_ GitLab.
With [Multiple Issue Boards](#multiple-issue-boards), available
only in [GitLab Enterprise Edition](https://about.gitlab.com/products/),
only in [GitLab Ultimate](https://about.gitlab.com/products/),
you go even further, as you can not only keep yourself and your project
organized from a broader perspective with one Issue Board per project,
but also allow your team members to organize their own workflow by creating
......
......@@ -316,6 +316,47 @@ or various static site generators. Contributions are very welcome.
Visit the GitLab Pages group for a full list of example projects:
<https://gitlab.com/groups/pages>.
### Serving compressed assets
Most modern browsers support downloading files in a compressed format. This
speeds up downloads by reducing the size of files.
Before serving an uncompressed file, Pages will check whether the same file
exists with a `.gz` extension. If it does, and the browser supports receiving
compressed files, it will serve that version instead of the uncompressed one.
To take advantage of this feature, the artifact you upload to the Pages should
have this structure:
```
public/
├─┬ index.html
│ └ index.html.gz
├── css/
│   └─┬ main.css
│ └ main.css.gz
└── js/
└─┬ main.js
└ main.js.gz
```
This can be achieved by including a `script:` command like this in your
`.gitlab-ci.yml` pages job:
```yaml
pages:
# Other directives
script:
- # build the public/ directory first
- find public -type f -iregex '.*\.\(htm\|html\|txt\|text\|js\|css\)$' -execdir gzip -f --keep {} \;
```
By pre-compressing the files and including both versions in the artifact, Pages
can serve requests for both compressed and uncompressed content without
needing to compress files on-demand.
### Add a custom domain to your Pages website
For a complete guide on Pages domains, read through the article
......
......@@ -68,7 +68,7 @@ You can live preview changes submitted to a new branch with
With [GitLab Enterprise Edition](https://about.gitlab.com/products/)
subscriptions, you can also request
[approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals) from your managers.
[approval](../merge_requests/merge_request_approvals.md) from your managers.
To create, delete, and [branches](branches/index.md) via GitLab's UI:
......
......@@ -6,6 +6,8 @@ module Gitlab
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e
raise FormatError, e.message
end
def valid?
......
......@@ -85,7 +85,7 @@ module Gitlab
begin
Gitlab::Ci::YamlProcessor.new(content, opts)
nil
rescue ValidationError, Psych::SyntaxError => e
rescue ValidationError => e
e.message
end
end
......
......@@ -402,15 +402,6 @@ module Gitlab
end
end
# Get a collection of Rugged::Reference objects for this commit.
#
# Ex.
# commit.ref(repo)
#
def refs(repo)
repo.refs_hash[id]
end
# Get ref names collection
#
# Ex.
......@@ -418,7 +409,7 @@ module Gitlab
#
def ref_names(repo)
refs(repo).map do |ref|
ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
ref.sub(%r{^refs/(heads|remotes|tags)/}, "")
end
end
......@@ -553,6 +544,15 @@ module Gitlab
date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i)
)
end
# Get a collection of Gitlab::Git::Ref objects for this commit.
#
# Ex.
# commit.ref(repo)
#
def refs(repo)
repo.refs_hash[id]
end
end
end
end
......@@ -631,21 +631,18 @@ module Gitlab
end
end
# Get refs hash which key is SHA1
# and value is a Rugged::Reference
# Get refs hash which key is is the commit id
# and value is a Gitlab::Git::Tag or Gitlab::Git::Branch
# Note that both inherit from Gitlab::Git::Ref
def refs_hash
# Initialize only when first call
if @refs_hash.nil?
@refs_hash = Hash.new { |h, k| h[k] = [] }
rugged.references.each do |r|
# Symbolic/remote references may not have an OID; skip over them
target_oid = r.target.try(:oid)
if target_oid
sha = rev_parse_target(target_oid).oid
@refs_hash[sha] << r
end
end
return @refs_hash if @refs_hash
@refs_hash = Hash.new { |h, k| h[k] = [] }
(tags + branches).each do |ref|
next unless ref.target && ref.name
@refs_hash[ref.dereferenced_target.id] << ref.name
end
@refs_hash
......
......@@ -25,9 +25,8 @@ module Gitlab
@repository.exists?
end
# Disabled because of https://gitlab.com/gitlab-org/gitaly/merge_requests/539
def write_page(name, format, content, commit_details)
@repository.gitaly_migrate(:wiki_write_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
@repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
if is_enabled
gitaly_write_page(name, format, content, commit_details)
gollum_wiki.clear_cache
......@@ -48,9 +47,8 @@ module Gitlab
end
end
# Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42094
def update_page(page_path, title, format, content, commit_details)
@repository.gitaly_migrate(:wiki_update_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
@repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
if is_enabled
gitaly_update_page(page_path, title, format, content, commit_details)
gollum_wiki.clear_cache
......
......@@ -222,14 +222,25 @@ module Gitlab
end
def find_commit(revision)
request = Gitaly::FindCommitRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
response.commit
if RequestStore.active?
# We don't use RequeStstore.fetch(key) { ... } directly because `revision`
# can be a branch name, so we can't use it as a key as it could point
# to another commit later on (happens a lot in tests).
key = {
storage: @gitaly_repo.storage_name,
relative_path: @gitaly_repo.relative_path,
commit_id: revision
}
return RequestStore[key] if RequestStore.exist?(key)
commit = call_find_commit(revision)
return unless commit
key[:commit_id] = commit.id
RequestStore[key] = commit
else
call_find_commit(revision)
end
end
def patch(revision)
......@@ -346,6 +357,17 @@ module Gitlab
def encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
end
def call_find_commit(revision)
request = Gitaly::FindCommitRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
response.commit
end
end
end
end
......@@ -4,7 +4,7 @@ module Gitlab
class AdditionalMetricsDeploymentQuery < BaseQuery
include QueryAdditionalMetrics
def query(deployment_id)
def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment|
query_metrics(
common_query_context(
......
......@@ -2,7 +2,7 @@ module Gitlab
module Prometheus
module Queries
class DeploymentQuery < BaseQuery
def query(deployment_id)
def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment|
environment_slug = deployment.environment.slug
......
......@@ -3,10 +3,10 @@ module Gitlab
# Helper methods to interact with Prometheus network services & resources
class PrometheusClient
attr_reader :api_url
attr_reader :rest_client, :headers
def initialize(api_url:)
@api_url = api_url
def initialize(rest_client)
@rest_client = rest_client
end
def ping
......@@ -40,37 +40,40 @@ module Gitlab
private
def json_api_get(type, args = {})
get(join_api_url(type, args))
path = ['api', 'v1', type].join('/')
get(path, args)
rescue JSON::ParserError
raise PrometheusError, 'Parsing response failed'
rescue Errno::ECONNREFUSED
raise PrometheusError, 'Connection refused'
end
def join_api_url(type, args = {})
url = URI.parse(api_url)
rescue URI::Error
raise PrometheusError, "Invalid API URL: #{api_url}"
else
url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
url.query = args.to_query
url.to_s
end
def get(url)
handle_response(HTTParty.get(url))
def get(path, args)
response = rest_client[path].get(params: args)
handle_response(response)
rescue SocketError
raise PrometheusError, "Can't connect to #{url}"
raise PrometheusError, "Can't connect to #{rest_client.url}"
rescue OpenSSL::SSL::SSLError
raise PrometheusError, "#{url} contains invalid SSL data"
rescue HTTParty::Error
raise PrometheusError, "#{rest_client.url} contains invalid SSL data"
rescue RestClient::ExceptionWithResponse => ex
handle_exception_response(ex.response)
rescue RestClient::Exception
raise PrometheusError, "Network connection error"
end
def handle_response(response)
if response.code == 200 && response['status'] == 'success'
response['data'] || {}
elsif response.code == 400
raise PrometheusError, response['error'] || 'Bad data received'
json_data = JSON.parse(response.body)
if response.code == 200 && json_data['status'] == 'success'
json_data['data'] || {}
else
raise PrometheusError, "#{response.code} - #{response.body}"
end
end
def handle_exception_response(response)
if response.code == 400
json_data = JSON.parse(response.body)
raise PrometheusError, json_data['error'] || 'Bad data received'
else
raise PrometheusError, "#{response.code} - #{response.body}"
end
......
......@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-07 14:09+0100\n"
"PO-Revision-Date: 2018-02-07 14:09+0100\n"
"POT-Creation-Date: 2018-02-07 13:35+0100\n"
"PO-Revision-Date: 2018-02-07 13:35+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -2515,7 +2515,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr "Nessuna metrica è stata monitorata. Per iniziare a monitorare, rilascia
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2032,7 +2032,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2018,7 +2018,7 @@ msgstr "Nenhuma métrica está sendo monitorada. Para inicar o monitoramento, fa
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "URL da API base do Prometheus. como http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Monitoramento com Prometheus"
msgid "PrometheusService|View environments"
......
......@@ -2032,7 +2032,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2032,7 +2032,7 @@ msgstr "Жодні метрики не відслідковуються. Для
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Базова адреса Prometheus API, наприклад http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Моніторинг Prometheus"
msgid "PrometheusService|View environments"
......
......@@ -2004,7 +2004,7 @@ msgstr "没有监测指标。要开始监测,请部署到环境中。"
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Prometheus API 地址,例如 http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Prometheus 监测"
msgid "PrometheusService|View environments"
......
......@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|View environments"
......
......@@ -297,7 +297,8 @@ FactoryBot.define do
project.create_prometheus_service(
active: true,
properties: {
api_url: 'https://prometheus.example.com'
api_url: 'https://prometheus.example.com/',
manual_configuration: true
}
)
end
......
......@@ -30,7 +30,8 @@ FactoryBot.define do
project
active true
properties({
api_url: 'https://prometheus.example.com/'
api_url: 'https://prometheus.example.com/',
manual_configuration: true
})
end
......
require 'spec_helper'
# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
describe 'User views a wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_page) do
create(:wiki_page,
wiki: project.wiki,
attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' })
end
before do
project.add_master(user)
sign_in(user)
end
describe 'User views a wiki page' do
shared_examples 'wiki page user view' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_page) do
create(:wiki_page,
wiki: project.wiki,
attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' })
end
context 'when wiki is empty' do
before do
visit(project_wikis_path(project))
project.add_master(user)
sign_in(user)
end
click_on('New page')
context 'when wiki is empty' do
before do
visit(project_wikis_path(project))
page.within('#modal-new-wiki') do
fill_in(:new_wiki_path, with: 'one/two/three-test')
click_on('Create page')
end
click_on('New page')
page.within('.wiki-form') do
fill_in(:wiki_content, with: 'wiki content')
click_on('Create page')
page.within('#modal-new-wiki') do
fill_in(:new_wiki_path, with: 'one/two/three-test')
click_on('Create page')
end
page.within('.wiki-form') do
fill_in(:wiki_content, with: 'wiki content')
click_on('Create page')
end
end
end
it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test')
it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test')
first(:link, text: 'Three').click
click_on('Page history')
first(:link, text: 'Three').click
click_on('Page history')
expect(current_path).to include('one/two/three-test')
expect(current_path).to include('one/two/three-test')
page.within(:css, '.nav-text') do
expect(page).to have_content('History')
page.within(:css, '.nav-text') do
expect(page).to have_content('History')
end
end
end
it 'shows an old version of a page', :js do
expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three')
it 'shows an old version of a page', :js do
expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three')
first(:link, text: 'Three').click
first(:link, text: 'Three').click
expect(find('.nav-text')).to have_content('Three')
expect(find('.nav-text')).to have_content('Three')
click_on('Edit')
click_on('Edit')
expect(current_path).to include('one/two/three-test')
expect(page).to have_content('Edit Page')
expect(current_path).to include('one/two/three-test')
expect(page).to have_content('Edit Page')
fill_in('Content', with: 'Updated Wiki Content')
fill_in('Content', with: 'Updated Wiki Content')
click_on('Save changes')
click_on('Page history')
click_on('Save changes')
click_on('Page history')
page.within(:css, '.nav-text') do
expect(page).to have_content('History')
end
page.within(:css, '.nav-text') do
expect(page).to have_content('History')
end
find('a[href*="?version_id"]')
find('a[href*="?version_id"]')
end
end
end
context 'when a page does not have history' do
before do
visit(project_wiki_path(project, wiki_page))
end
context 'when a page does not have history' do
before do
visit(project_wiki_path(project, wiki_page))
end
it 'shows all the pages' do
expect(page).to have_content(user.name)
expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
end
it 'shows all the pages' do
expect(page).to have_content(user.name)
expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
end
it 'shows a file stored in a page' do
gollum_file_double = double('Gollum::File',
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
it 'shows a file stored in a page' do
gollum_file_double = double('Gollum::File',
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath('//img[@data-src="image.jpg"]')
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
expect(page).to have_xpath('//img[@data-src="image.jpg"]')
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
click_on('image')
click_on('image')
expect(current_path).to match('wikis/image.jpg')
expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end
expect(current_path).to match('wikis/image.jpg')
expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end
it 'shows the creation page if file does not exist' do
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
it 'shows the creation page if file does not exist' do
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
click_on('image')
click_on('image')
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
expect(page).to have_content('Create page')
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
expect(page).to have_content('Create page')
end
end
end
context 'when a page has history' do
before do
wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)')
end
context 'when a page has history' do
before do
wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)')
end
it 'shows the page history' do
visit(project_wiki_path(project, wiki_page))
it 'shows the page history' do
visit(project_wiki_path(project, wiki_page))
expect(page).to have_selector('a.btn', text: 'Edit')
expect(page).to have_selector('a.btn', text: 'Edit')
click_on('Page history')
click_on('Page history')
expect(page).to have_content(user.name)
expect(page).to have_content("#{user.username} created page: home")
expect(page).to have_content('updated home')
expect(page).to have_content(user.name)
expect(page).to have_content("#{user.username} created page: home")
expect(page).to have_content('updated home')
end
it 'does not show the "Edit" button' do
visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id))
expect(page).not_to have_selector('a.btn', text: 'Edit')
end
end
it 'does not show the "Edit" button' do
visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id))
it 'opens a default wiki page', :js do
visit(project_path(project))
expect(page).not_to have_selector('a.btn', text: 'Edit')
find('.shortcuts-wiki').click
expect(page).to have_content('Home · Create Page')
end
end
it 'opens a default wiki page', :js do
visit(project_path(project))
find('.shortcuts-wiki').click
context 'when Gitaly is enabled' do
it_behaves_like 'wiki page user view'
end
expect(page).to have_content('Home · Create Page')
context 'when Gitaly is disabled', :skip_gitaly_mock do
it_behaves_like 'wiki page user view'
end
end
......@@ -7,10 +7,10 @@ describe GraphHelper do
let(:graph) { Network::Graph.new(project, 'master', commit, '') }
it 'filters our refs used by GitLab' do
allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz'])
self.instance_variable_set(:@graph, graph)
refs = get_refs(project.repository, commit)
expect(refs).to eq('master')
refs = refs(project.repository, commit)
expect(refs).to match('master')
end
end
end
......@@ -105,4 +105,68 @@ describe('dateInWords', () => {
it('should return abbreviated month name', () => {
expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
});
it('should return date in words without year', () => {
expect(datetimeUtility.dateInWords(date, true, true)).toEqual('Jul 1');
});
});
describe('monthInWords', () => {
const date = new Date('2017-01-20');
it('returns month name from provided date', () => {
expect(datetimeUtility.monthInWords(date)).toBe('January');
});
it('returns abbreviated month name from provided date', () => {
expect(datetimeUtility.monthInWords(date, true)).toBe('Jan');
});
});
describe('totalDaysInMonth', () => {
it('returns number of days in a month for given date', () => {
// 1st Feb, 2016 (leap year)
expect(datetimeUtility.totalDaysInMonth(new Date(2016, 1, 1))).toBe(29);
// 1st Feb, 2017
expect(datetimeUtility.totalDaysInMonth(new Date(2017, 1, 1))).toBe(28);
// 1st Jan, 2017
expect(datetimeUtility.totalDaysInMonth(new Date(2017, 0, 1))).toBe(31);
});
});
describe('getSundays', () => {
it('returns array of dates representing all Sundays of the month', () => {
// December, 2017 (it has 5 Sundays)
const dateOfSundays = [3, 10, 17, 24, 31];
const sundays = datetimeUtility.getSundays(new Date(2017, 11, 1));
expect(sundays.length).toBe(5);
sundays.forEach((sunday, index) => {
expect(sunday.getDate()).toBe(dateOfSundays[index]);
});
});
});
describe('getTimeframeWindow', () => {
it('returns array of dates representing a timeframe based on provided length and date', () => {
const date = new Date(2018, 0, 1);
const mockTimeframe = [
new Date(2017, 9, 1),
new Date(2017, 10, 1),
new Date(2017, 11, 1),
new Date(2018, 0, 1),
new Date(2018, 1, 1),
new Date(2018, 2, 31),
];
const timeframe = datetimeUtility.getTimeframeWindow(6, date);
expect(timeframe.length).toBe(6);
timeframe.forEach((timeframeItem, index) => {
expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBeTruthy();
expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBeTruthy();
expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy();
});
});
});
......@@ -480,4 +480,33 @@ describe('common_utils', () => {
expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>');
});
});
describe('convertObjectPropsToCamelCase', () => {
it('returns new object with camelCase property names by converting object with snake_case names', () => {
const snakeRegEx = /(_\w)/g;
const mockObj = {
id: 1,
group_name: 'GitLab.org',
absolute_web_url: 'https://gitlab.com/gitlab-org/',
};
const mappings = {
id: 'id',
groupName: 'group_name',
absoluteWebUrl: 'absolute_web_url',
};
const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj);
Object.keys(convertedObj).forEach((prop) => {
expect(snakeRegEx.test(prop)).toBeFalsy();
expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]);
});
});
it('return empty object if method is called with null or undefined', () => {
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase(null)).length).toBe(0);
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0);
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0);
});
});
});
......@@ -70,4 +70,10 @@ describe('text_utility', () => {
expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
});
});
describe('convertToCamelCase', () => {
it('converts snake_case string to camelCase string', () => {
expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase');
});
});
});
......@@ -29,34 +29,6 @@ describe('EmptyState', () => {
expect(component.currentState).toBe(component.states.gettingStarted);
});
it('buttonPath returns settings path for the state "gettingStarted"', () => {
const component = createComponent({
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo',
emptyUnableToConnectSvgPath: 'foo',
});
expect(component.buttonPath).toEqual(statePaths.settingsPath);
expect(component.buttonPath).not.toEqual(statePaths.documentationPath);
});
it('buttonPath returns documentation path for any of the other states', () => {
const component = createComponent({
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo',
emptyUnableToConnectSvgPath: 'foo',
});
expect(component.buttonPath).toEqual(statePaths.documentationPath);
expect(component.buttonPath).not.toEqual(statePaths.settingsPath);
});
it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
......@@ -88,6 +60,7 @@ describe('EmptyState', () => {
const component = createComponent({
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
clustersPath: statePaths.clustersPath,
documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo',
......
......@@ -2471,6 +2471,7 @@ export const deploymentData = [
export const statePaths = {
settingsPath: '/root/hello-prometheus/services/prometheus/edit',
clustersPath: '/root/hello-prometheus/clusters',
documentationPath: '/help/administration/monitoring/prometheus/index.md',
};
......
......@@ -247,5 +247,44 @@ describe Gitlab::Checks::ChangeAccess do
end
end
end
context 'LFS file lock check' do
let(:owner) { create(:user) }
let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end
context 'with LFS not enabled' do
it 'skips the validation' do
expect_any_instance_of(described_class).not_to receive(:lfs_file_locks_validation)
subject.exec
end
end
context 'with LFS enabled' do
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
context 'when change is sent by a different user' do
it 'raises an error if the user is not allowed to update the file' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
end
end
context 'when change is sent by the author od the lock' do
let(:user) { owner }
it "doesn't raise any error" do
expect { subject.exec }.not_to raise_error
end
end
end
end
end
end
......@@ -38,6 +38,16 @@ describe Gitlab::Ci::Config::Loader do
end
end
context 'when there is an unknown alias' do
let(:yml) { 'steps: *bad_alias' }
describe '#initialize' do
it 'raises FormatError' do
expect { loader }.to raise_error(Gitlab::Ci::Config::Loader::FormatError, 'Unknown alias: bad_alias')
end
end
end
context 'when yaml config is empty' do
let(:yml) { '' }
......
......@@ -1394,11 +1394,15 @@ EOT
describe "Error handling" do
it "fails to parse YAML" do
expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
expect do
Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
end
it "indicates that object is invalid" do
expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
expect do
Gitlab::Ci::YamlProcessor.new("invalid_yaml")
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
end
it "returns errors if tags parameter is invalid" do
......@@ -1688,37 +1692,36 @@ EOT
end
describe "#validation_message" do
subject { Gitlab::Ci::YamlProcessor.validation_message(content) }
context "when the YAML could not be parsed" do
it "returns an error about invalid configutaion" do
content = YAML.dump("invalid: yaml: test")
let(:content) { YAML.dump("invalid: yaml: test") }
expect(Gitlab::Ci::YamlProcessor.validation_message(content))
.to eq "Invalid configuration format"
end
it { is_expected.to eq "Invalid configuration format" }
end
context "when the tags parameter is invalid" do
it "returns an error about invalid tags" do
content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
let(:content) { YAML.dump({ rspec: { script: "test", tags: "mysql" } }) }
expect(Gitlab::Ci::YamlProcessor.validation_message(content))
.to eq "jobs:rspec tags should be an array of strings"
end
it { is_expected.to eq "jobs:rspec tags should be an array of strings" }
end
context "when YAML content is empty" do
it "returns an error about missing content" do
expect(Gitlab::Ci::YamlProcessor.validation_message(''))
.to eq "Please provide content of .gitlab-ci.yml"
end
let(:content) { '' }
it { is_expected.to eq "Please provide content of .gitlab-ci.yml" }
end
context 'when the YAML contains an unknown alias' do
let(:content) { 'steps: *bad_alias' }
it { is_expected.to eq "Unknown alias: bad_alias" }
end
context "when the YAML is valid" do
it "does not return any errors" do
content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
let(:content) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil
end
it { is_expected.to be_nil }
end
end
......
......@@ -600,12 +600,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#refs_hash" do
let(:refs) { repository.refs_hash }
subject { repository.refs_hash }
it "should have as many entries as branches and tags" do
expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
# We flatten in case a commit is pointed at by more than one branch and/or tag
expect(refs.values.flatten.size).to eq(expected_refs.size)
expect(subject.values.flatten.size).to eq(expected_refs.size)
end
it 'has valid commit ids as keys' do
expect(subject.keys).to all( match(Commit::COMMIT_SHA_PATTERN) )
end
end
......
......@@ -3,34 +3,38 @@ require 'spec_helper'
describe Gitlab::Git::Wiki do
let(:project) { create(:project) }
let(:user) { project.owner }
let(:wiki) { ProjectWiki.new(project, user) }
let(:gollum_wiki) { wiki.wiki }
let(:project_wiki) { ProjectWiki.new(project, user) }
subject { project_wiki.wiki }
# Remove skip_gitaly_mock flag when gitaly_find_page when
# https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged
# https://gitlab.com/gitlab-org/gitlab-ce/issues/42039 is solved
describe '#page', :skip_gitaly_mock do
it 'returns the right page' do
before do
create_page('page1', 'content')
create_page('foo/page1', 'content')
expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1'
expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
create_page('foo/page1', 'content foo/page1')
end
after do
destroy_page('page1')
destroy_page('page1', 'foo')
end
it 'returns the right page' do
expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1'
expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
end
end
def create_page(name, content)
gollum_wiki.write_page(name, :markdown, content, commit_details)
subject.write_page(name, :markdown, content, commit_details(name))
end
def commit_details
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
def commit_details(name)
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}")
end
def destroy_page(title, dir = '')
page = gollum_wiki.page(title: title, dir: dir)
wiki.delete_page(page, "test commit")
page = subject.page(title: title, dir: dir)
project_wiki.delete_page(page, "test commit")
end
end
......@@ -166,6 +166,32 @@ describe Gitlab::GitalyClient::CommitService do
described_class.new(repository).find_commit(revision)
end
describe 'caching', :request_store do
let(:commit_dbl) { double(id: 'f01b' * 10) }
context 'when passed revision is a branch name' do
it 'calls Gitaly' do
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).twice.and_return(double(commit: commit_dbl))
commit = nil
2.times { commit = described_class.new(repository).find_commit('master') }
expect(commit).to eq(commit_dbl)
end
end
context 'when passed revision is a commit ID' do
it 'returns a cached commit' do
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl))
commit = nil
2.times { commit = described_class.new(repository).find_commit('f01b' * 10) }
expect(commit).to eq(commit_dbl)
end
end
end
end
describe '#patch' do
......
......@@ -7,7 +7,7 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do
include_examples 'additional metrics query' do
let(:deployment) { create(:deployment, environment: environment) }
let(:query_params) { [deployment.id] }
let(:query_params) { [environment.id, deployment.id] }
it 'queries using specific time' do
expect(client).to receive(:query_range).with(anything,
......
......@@ -31,7 +31,7 @@ describe Gitlab::Prometheus::Queries::DeploymentQuery do
expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
time: stop_time)
expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil,
cpu_values: nil, cpu_before: nil, cpu_after: nil)
expect(subject.query(environment.id, deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil,
cpu_values: nil, cpu_before: nil, cpu_after: nil)
end
end
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::PrometheusClient do
include PrometheusHelpers
subject { described_class.new(api_url: 'https://prometheus.example.com') }
subject { described_class.new(RestClient::Resource.new('https://prometheus.example.com')) }
describe '#ping' do
it 'issues a "query" request to the API endpoint' do
......@@ -47,16 +47,28 @@ describe Gitlab::PrometheusClient do
expect(req_stub).to have_been_requested
end
end
context 'when request returns non json data' do
it 'raises a Gitlab::PrometheusError error' do
req_stub = stub_prometheus_request(query_url, status: 200, body: 'not json')
expect { execute_query }
.to raise_error(Gitlab::PrometheusError, 'Parsing response failed')
expect(req_stub).to have_been_requested
end
end
end
describe 'failure to reach a provided prometheus url' do
let(:prometheus_url) {"https://prometheus.invalid.example.com"}
subject { described_class.new(RestClient::Resource.new(prometheus_url)) }
context 'exceptions are raised' do
it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
expect { subject.send(:get, prometheus_url) }
expect { subject.send(:get, '/', {}) }
.to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested
end
......@@ -64,15 +76,15 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
expect { subject.send(:get, prometheus_url) }
expect { subject.send(:get, '/', {}) }
.to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested
end
it 'raises a Gitlab::PrometheusError error when a HTTParty::Error is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, HTTParty::Error)
it 'raises a Gitlab::PrometheusError error when a RestClient::Exception is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception)
expect { subject.send(:get, prometheus_url) }
expect { subject.send(:get, '/', {}) }
.to raise_error(Gitlab::PrometheusError, "Network connection error")
expect(req_stub).to have_been_requested
end
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180119121225_remove_redundant_pipeline_stages.rb')
describe RemoveRedundantPipelineStages, :migration do
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:builds) { table(:ci_builds) }
before do
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
pipelines.create!(id: 234, project_id: 123, ref: 'master', sha: 'adf43c3a')
stages.create!(id: 6, project_id: 123, pipeline_id: 234, name: 'build')
stages.create!(id: 10, project_id: 123, pipeline_id: 234, name: 'build')
stages.create!(id: 21, project_id: 123, pipeline_id: 234, name: 'build')
stages.create!(id: 41, project_id: 123, pipeline_id: 234, name: 'test')
stages.create!(id: 62, project_id: 123, pipeline_id: 234, name: 'test')
stages.create!(id: 102, project_id: 123, pipeline_id: 234, name: 'deploy')
builds.create!(id: 1, commit_id: 234, project_id: 123, stage_id: 10)
builds.create!(id: 2, commit_id: 234, project_id: 123, stage_id: 21)
builds.create!(id: 3, commit_id: 234, project_id: 123, stage_id: 21)
builds.create!(id: 4, commit_id: 234, project_id: 123, stage_id: 41)
builds.create!(id: 5, commit_id: 234, project_id: 123, stage_id: 62)
builds.create!(id: 6, commit_id: 234, project_id: 123, stage_id: 102)
end
it 'removes ambiguous stages and preserves builds' do
expect(stages.all.count).to eq 6
expect(builds.all.count).to eq 6
migrate!
expect(stages.all.count).to eq 1
expect(builds.all.count).to eq 6
expect(builds.all.pluck(:stage_id).compact).to eq [102]
end
it 'retries when incorrectly added index exception is caught' do
allow_any_instance_of(described_class)
.to receive(:remove_redundant_pipeline_stages!)
expect_any_instance_of(described_class)
.to receive(:remove_outdated_index!)
.exactly(100).times.and_call_original
expect { migrate! }
.to raise_error StandardError, /Failed to add an unique index/
end
it 'does not retry when unknown exception is being raised' do
allow(subject).to receive(:remove_outdated_index!)
expect(subject).to receive(:remove_redundant_pipeline_stages!).once
allow(subject).to receive(:add_unique_index!).and_raise(StandardError)
expect { subject.up(attempts: 3) }.to raise_error StandardError
end
end
......@@ -6,6 +6,24 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application specs', described_class
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, projects: [project]) }
let(:prometheus_service) { double('prometheus_service') }
subject { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
before do
allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service
end
it 'ensures Prometheus service is activated' do
expect(prometheus_service).to receive(:update).with(active: true)
subject.make_installed
end
end
describe "#chart_values_file" do
subject { create(:clusters_applications_prometheus).chart_values_file }
......@@ -13,4 +31,58 @@ describe Clusters::Applications::Prometheus do
expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
end
end
describe '#proxy_client' do
context 'cluster is nil' do
it 'returns nil' do
expect(subject.cluster).to be_nil
expect(subject.proxy_client).to be_nil
end
end
context "cluster doesn't have kubeclient" do
let(:cluster) { create(:cluster) }
subject { create(:clusters_applications_prometheus, cluster: cluster) }
it 'returns nil' do
expect(subject.proxy_client).to be_nil
end
end
context 'cluster has kubeclient' do
let(:kubernetes_url) { 'http://example.com' }
let(:k8s_discover_response) do
{
resources: [
{
name: 'service',
kind: 'Service'
}
]
}
end
let(:kube_client) { Kubeclient::Client.new(kubernetes_url) }
let(:cluster) { create(:cluster) }
subject { create(:clusters_applications_prometheus, cluster: cluster) }
before do
allow(kube_client.rest_client).to receive(:get).and_return(k8s_discover_response.to_json)
allow(subject.cluster).to receive(:kubeclient).and_return(kube_client)
end
it 'creates proxy prometheus rest client' do
expect(subject.proxy_client).to be_instance_of(RestClient::Resource)
end
it 'creates proper url' do
expect(subject.proxy_client.url).to eq('http://example.com/api/v1/proxy/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80')
end
it 'copies options and headers from kube client to proxy client' do
expect(subject.proxy_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers))
end
end
end
end
......@@ -2471,7 +2471,7 @@ describe Project do
create(:ci_variable, :protected, value: 'protected', project: project)
end
subject { project.secret_variables_for(ref: 'ref') }
subject { project.reload.secret_variables_for(ref: 'ref') }
before do
stub_application_setting(
......
This diff is collapsed.
require 'spec_helper'
describe Ci::EnsureStageService, '#execute' do
set(:project) { create(:project) }
set(:user) { create(:user) }
let(:stage) { create(:ci_stage_entity) }
let(:job) { build(:ci_build) }
let(:service) { described_class.new(project, user) }
context 'when build has a stage assigned' do
it 'does not create a new stage' do
job.assign_attributes(stage_id: stage.id)
expect { service.execute(job) }.not_to change { Ci::Stage.count }
end
end
context 'when build does not have a stage assigned' do
it 'creates a new stage' do
job.assign_attributes(stage_id: nil, stage: 'test')
expect { service.execute(job) }.to change { Ci::Stage.count }.by(1)
end
end
context 'when build is invalid' do
it 'does not create a new stage' do
job.assign_attributes(stage_id: nil, ref: nil)
expect { service.execute(job) }.not_to change { Ci::Stage.count }
end
end
context 'when new stage can not be created because of an exception' do
before do
allow(Ci::Stage).to receive(:create!)
.and_raise(ActiveRecord::RecordNotUnique.new('Duplicates!'))
end
it 'retries up to two times' do
job.assign_attributes(stage_id: nil)
expect(service).to receive(:find_stage).exactly(2).times
expect { service.execute(job) }
.to raise_error(Ci::EnsureStageService::EnsureStageError)
end
end
end
......@@ -5,7 +5,11 @@ describe Ci::RetryBuildService do
set(:project) { create(:project) }
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:stage) do
Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test')
end
let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) }
let(:service) do
described_class.new(project, user)
......@@ -28,29 +32,27 @@ describe Ci::RetryBuildService do
sourced_pipelines artifacts_file_store artifacts_metadata_store].freeze # EE
shared_examples 'build duplication' do
let(:stage) do
# TODO, we still do not have factory for new stages, we will need to
# switch existing factory to persist stages, instead of using LegacyStage
#
Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test')
end
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let(:build) do
create(:ci_build, :failed, :artifacts, :expired, :erased,
:queued, :coverage, :tags, :allowed_to_fail, :on_tag,
:triggered, :trace_artifact, :teardown_environment,
description: 'my-job', stage: 'test', pipeline: pipeline,
auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build|
##
# TODO, workaround for FactoryBot limitation when having both
# stage (text) and stage_id (integer) columns in the table.
build.stage_id = stage.id
end
description: 'my-job', stage: 'test', stage_id: stage.id,
pipeline: pipeline, auto_canceled_by: another_pipeline)
end
before do
# Make sure that build has both `stage_id` and `stage` because FactoryBot
# can reset one of the fields when assigning another. We plan to deprecate
# and remove legacy `stage` column in the future.
build.update_attributes(stage: 'test', stage_id: stage.id)
end
describe 'clone accessors' do
CLONE_ACCESSORS.each do |attribute|
it "clones #{attribute} build attribute" do
expect(build.send(attribute)).not_to be_nil
expect(new_build.send(attribute)).not_to be_nil
expect(new_build.send(attribute)).to eq build.send(attribute)
end
......@@ -123,10 +125,12 @@ describe Ci::RetryBuildService do
context 'when there are subsequent builds that are skipped' do
let!(:subsequent_build) do
create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline)
create(:ci_build, :skipped, stage_idx: 2,
pipeline: pipeline,
stage: 'deploy')
end
it 'resumes pipeline processing in subsequent stages' do
it 'resumes pipeline processing in a subsequent stage' do
service.execute(build)
expect(subsequent_build.reload).to be_created
......
This diff is collapsed.
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