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 { ...@@ -32,6 +32,7 @@ export default class Clusters {
installIngressPath, installIngressPath,
installRunnerPath, installRunnerPath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath,
clusterStatus, clusterStatus,
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
...@@ -40,6 +41,7 @@ export default class Clusters { ...@@ -40,6 +41,7 @@ export default class Clusters {
this.store = new ClustersStore(); this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath); this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus); this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason); this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({ this.service = new ClustersService({
...@@ -95,6 +97,7 @@ export default class Clusters { ...@@ -95,6 +97,7 @@ export default class Clusters {
applications: this.state.applications, applications: this.state.applications,
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath, ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
}, },
}); });
}, },
......
...@@ -32,6 +32,10 @@ ...@@ -32,6 +32,10 @@
type: String, type: String,
required: false, required: false,
}, },
manageLink: {
type: String,
required: false,
},
description: { description: {
type: String, type: String,
required: true, required: true,
...@@ -89,6 +93,12 @@ ...@@ -89,6 +93,12 @@
return label; return label;
}, },
showManageButton() {
return this.manageLink && this.status === APPLICATION_INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() { hasError() {
return this.status === APPLICATION_ERROR || return this.status === APPLICATION_ERROR ||
this.requestStatus === REQUEST_FAILURE; this.requestStatus === REQUEST_FAILURE;
...@@ -141,9 +151,21 @@ ...@@ -141,9 +151,21 @@
<div v-html="description"></div> <div v-html="description"></div>
</div> </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" 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"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
class="js-cluster-application-install-button" class="js-cluster-application-install-button"
......
...@@ -23,13 +23,19 @@ ...@@ -23,13 +23,19 @@
required: false, required: false,
default: '', default: '',
}, },
managePrometheusPath: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
generalApplicationDescription() { generalApplicationDescription() {
return sprintf( return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster. _.escape(s__(
Read more about %{helpLink}`)), `ClusterIntegration|Install applications on your Kubernetes cluster.
{ Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}"> helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))} ${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`, </a>`,
...@@ -96,11 +102,12 @@ ...@@ -96,11 +102,12 @@
}, },
prometheusDescription() { prometheusDescription() {
return sprintf( return sprintf(
_.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system _.escape(s__(
with %{gitlabIntegrationLink} to monitor deployed applications.`)), `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" 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>`, ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
}, },
false, false,
...@@ -149,6 +156,7 @@ target="_blank" rel="noopener noreferrer"> ...@@ -149,6 +156,7 @@ target="_blank" rel="noopener noreferrer">
id="prometheus" id="prometheus"
:title="applications.prometheus.title" :title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:description="prometheusDescription" :description="prometheusDescription"
:status="applications.prometheus.status" :status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
......
...@@ -45,6 +45,10 @@ export default class ClusterStore { ...@@ -45,6 +45,10 @@ export default class ClusterStore {
this.state.ingressHelpPath = ingressHelpPath; this.state.ingressHelpPath = ingressHelpPath;
} }
setManagePrometheusPath(managePrometheusPath) {
this.state.managePrometheusPath = managePrometheusPath;
}
updateStatus(status) { updateStatus(status) {
this.state.status = status; this.state.status = status;
} }
......
import axios from './axios_utils'; import axios from './axios_utils';
import { getLocationHash } from './url_utility'; import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
...@@ -395,6 +396,26 @@ export const spriteIcon = (icon, className = '') => { ...@@ -395,6 +396,26 @@ export const spriteIcon = (icon, className = '') => {
return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`; 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}`; export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -9,6 +9,20 @@ import { ...@@ -9,6 +9,20 @@ import {
window.timeago = timeago; window.timeago = timeago;
window.dateFormat = dateFormat; 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 * Given a date object returns the day of the week in English
* @param {date} date * @param {date} date
...@@ -157,7 +171,7 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -157,7 +171,7 @@ export function timeIntervalInWords(intervalInSeconds) {
return text; return text;
} }
export function dateInWords(date, abbreviated = false) { export function dateInWords(date, abbreviated = false, hideYear = false) {
if (!date) return date; if (!date) return date;
const month = date.getMonth(); const month = date.getMonth();
...@@ -168,9 +182,115 @@ export function dateInWords(date, abbreviated = false) { ...@@ -168,9 +182,115 @@ export function dateInWords(date, abbreviated = false) {
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month]; const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
if (hideYear) {
return `${monthName} ${date.getDate()}`;
}
return `${monthName} ${date.getDate()}, ${year}`; 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 = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
...@@ -73,3 +73,10 @@ export function capitalizeFirstCharacter(text) { ...@@ -73,3 +73,10 @@ export function capitalizeFirstCharacter(text) {
* @returns {String} * @returns {String}
*/ */
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); 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 @@ ...@@ -27,6 +27,7 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath, documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath, settingsPath: metricsData.settingsPath,
clustersPath: metricsData.clustersPath,
tagsPath: metricsData.tagsPath, tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath, projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics, metricsEndpoint: metricsData.additionalMetrics,
...@@ -132,6 +133,7 @@ ...@@ -132,6 +133,7 @@
:selected-state="state" :selected-state="state"
:documentation-path="documentationPath" :documentation-path="documentationPath"
:settings-path="settingsPath" :settings-path="settingsPath"
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
......
...@@ -10,6 +10,11 @@ ...@@ -10,6 +10,11 @@
required: false, required: false,
default: '', default: '',
}, },
clustersPath: {
type: String,
required: false,
default: '',
},
selectedState: { selectedState: {
type: String, type: String,
required: true, required: true,
...@@ -35,7 +40,10 @@ ...@@ -35,7 +40,10 @@
title: 'Get started with performance monitoring', title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`, 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: { loading: {
svgUrl: this.emptyLoadingSvgPath, svgUrl: this.emptyLoadingSvgPath,
...@@ -43,6 +51,7 @@ ...@@ -43,6 +51,7 @@
description: `Creating graphs uses the data from the Prometheus server. description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`, If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation', buttonText: 'View documentation',
buttonPath: this.documentationPath,
}, },
noData: { noData: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
...@@ -50,12 +59,14 @@ ...@@ -50,12 +59,14 @@
description: `You are connected to the Prometheus server, but there is currently description: `You are connected to the Prometheus server, but there is currently
no data to display.`, no data to display.`,
buttonText: 'Configure Prometheus', buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath,
}, },
unableToConnect: { unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server', title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ', description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation', buttonText: 'View documentation',
buttonPath: this.documentationPath,
}, },
}, },
}; };
...@@ -65,13 +76,6 @@ ...@@ -65,13 +76,6 @@
return this.states[this.selectedState]; return this.states[this.selectedState];
}, },
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() { showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true; if (this.selectedState === 'unableToConnect') return true;
return false; return false;
...@@ -99,11 +103,21 @@ ...@@ -99,11 +103,21 @@
</p> </p>
<div class="state-button"> <div class="state-button">
<a <a
v-if="currentState.buttonPath"
class="btn btn-success" class="btn btn-success"
:href="buttonPath" :href="currentState.buttonPath"
> >
{{ currentState.buttonText }} {{ currentState.buttonText }}
</a> </a>
</div> </div>
<div class="state-button">
<a
v-if="currentState.secondaryButtonPath"
class="btn"
:href="currentState.secondaryButtonPath"
>
{{ currentState.secondaryButtonText }}
</a>
</div>
</div> </div>
</template> </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 */ /* 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'; import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
/** /**
...@@ -146,23 +147,25 @@ export default class SearchAutocomplete { ...@@ -146,23 +147,25 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true; this.loadingSuggestions = true;
return $.get(this.autocompletePath, { return axios.get(this.autocompletePath, {
project_id: this.projectId, params: {
project_ref: this.projectRef, project_id: this.projectId,
term: term, project_ref: this.projectRef,
}, (response) => { term: term,
var firstCategory, i, lastCategory, len, suggestion; },
}).then((response) => {
// Hide dropdown menu if no suggestions returns // Hide dropdown menu if no suggestions returns
if (!response.length) { if (!response.data.length) {
this.disableAutocomplete(); this.disableAutocomplete();
return; return;
} }
const data = []; const data = [];
// List results // List results
firstCategory = true; let firstCategory = true;
for (i = 0, len = response.length; i < len; i += 1) { let lastCategory;
suggestion = response[i]; for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group // Add group header before list each group
if (lastCategory !== suggestion.category) { if (lastCategory !== suggestion.category) {
if (!firstCategory) { if (!firstCategory) {
...@@ -177,7 +180,7 @@ export default class SearchAutocomplete { ...@@ -177,7 +180,7 @@ export default class SearchAutocomplete {
lastCategory = suggestion.category; lastCategory = suggestion.category;
} }
data.push({ data.push({
id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
category: suggestion.category, category: suggestion.category,
text: suggestion.label, text: suggestion.label,
url: suggestion.url, url: suggestion.url,
...@@ -187,13 +190,17 @@ export default class SearchAutocomplete { ...@@ -187,13 +190,17 @@ export default class SearchAutocomplete {
if (data.length) { if (data.length) {
data.push('separator'); data.push('separator');
data.push({ data.push({
text: "Result name contains \"" + term + "\"", text: `Result name contains "${term}"`,
url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
}); });
} }
return callback(data);
}) callback(data);
.always(() => { this.loadingSuggestions = false; });
this.loadingSuggestions = false;
}).catch(() => {
this.loadingSuggestions = false;
});
} }
getCategoryContents() { getCategoryContents() {
......
...@@ -215,6 +215,7 @@ $tooltip-font-size: 12px; ...@@ -215,6 +215,7 @@ $tooltip-font-size: 12px;
* Padding * Padding
*/ */
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-8: 8px;
$gl-col-padding: 15px; $gl-col-padding: 15px;
$gl-btn-padding: 10px; $gl-btn-padding: 10px;
$gl-input-padding: 10px; $gl-input-padding: 10px;
......
...@@ -365,7 +365,7 @@ ...@@ -365,7 +365,7 @@
} }
.prometheus-state { .prometheus-state {
max-width: 430px; max-width: 460px;
margin: 10px auto; margin: 10px auto;
text-align: center; text-align: center;
...@@ -373,6 +373,10 @@ ...@@ -373,6 +373,10 @@
max-width: 80vw; max-width: 80vw;
margin: 0 auto; margin: 0 auto;
} }
.state-button {
padding: $gl-padding / 2;
}
} }
.environments-actions { .environments-actions {
......
...@@ -135,6 +135,17 @@ ...@@ -135,6 +135,17 @@
padding-top: 0; padding-top: 0;
} }
.integration-settings-form {
.well {
padding: $gl-padding / 2;
box-shadow: none;
}
.svg-container {
max-width: 150px;
}
}
.token-token-container { .token-token-container {
#impersonation-token-token { #impersonation-token-token {
width: 80%; width: 80%;
......
...@@ -16,10 +16,7 @@ module Ci ...@@ -16,10 +16,7 @@ module Ci
@builds = @config_processor.builds @builds = @config_processor.builds
@jobs = @config_processor.jobs @jobs = @config_processor.jobs
end end
rescue
@error = 'Undefined error'
@status = false
ensure
render :show render :show
end end
end end
......
...@@ -33,6 +33,7 @@ module ServiceParams ...@@ -33,6 +33,7 @@ module ServiceParams
:issues_events, :issues_events,
:issues_url, :issues_url,
:jira_issue_transition_id, :jira_issue_transition_id,
:manual_configuration,
:merge_requests_events, :merge_requests_events,
:mock_service_url, :mock_service_url,
:namespace, :namespace,
......
...@@ -53,10 +53,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -53,10 +53,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
set_pipeline_variables set_pipeline_variables
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37432 render
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
end end
format.json do format.json do
......
...@@ -28,7 +28,7 @@ class SnippetsFinder < UnionFinder ...@@ -28,7 +28,7 @@ class SnippetsFinder < UnionFinder
segments << items.public_to_user(current_user) segments << items.public_to_user(current_user)
segments << authorized_to_user(items) if current_user segments << authorized_to_user(items) if current_user
find_union(segments, Snippet) find_union(segments, Snippet.includes(:author))
end end
def authorized_to_user(items) def authorized_to_user(items)
......
module GraphHelper module GraphHelper
def get_refs(repo, commit) def refs(repo, commit)
refs = "" refs = commit.ref_names(repo).join(' ')
# 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(' ')
# append note count # append note count
notes_count = @graph.notes[commit.id] notes_count = @graph.notes[commit.id]
......
...@@ -407,7 +407,7 @@ module Ci ...@@ -407,7 +407,7 @@ module Ci
@config_processor ||= begin @config_processor ||= begin
initialize_yaml_processor initialize_yaml_processor
rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e rescue Gitlab::Ci::YamlProcessor::ValidationError => e
self.yaml_errors = e.message self.yaml_errors = e.message
nil nil
rescue rescue
......
...@@ -10,10 +10,26 @@ module Clusters ...@@ -10,10 +10,26 @@ module Clusters
default_value_for :version, VERSION 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 def chart
'stable/prometheus' 'stable/prometheus'
end end
def service_name
'prometheus-prometheus-server'
end
def service_port
80
end
def chart_values_file def chart_values_file
"#{Rails.root}/vendor/#{name}/values.yaml" "#{Rails.root}/vendor/#{name}/values.yaml"
end end
...@@ -21,6 +37,22 @@ module Clusters ...@@ -21,6 +37,22 @@ module Clusters
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end 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 end
end end
...@@ -51,6 +51,9 @@ module Clusters ...@@ -51,6 +51,9 @@ module Clusters
scope :enabled, -> { where(enabled: true) } scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) } scope :disabled, -> { where(enabled: false) }
scope :for_environment, -> (env) { where(environment_scope: ['*', '', env.slug]) }
scope :for_all_environments, -> { where(environment_scope: ['*', '']) }
def status_name def status_name
if provider if provider
provider.status_name provider.status_name
......
...@@ -290,7 +290,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -290,7 +290,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def keep_around_commits 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(start_commit_sha)
repo.keep_around(head_commit_sha) repo.keep_around(head_commit_sha)
repo.keep_around(base_commit_sha) repo.keep_around(base_commit_sha)
......
...@@ -7,11 +7,14 @@ class PrometheusService < MonitoringService ...@@ -7,11 +7,14 @@ class PrometheusService < MonitoringService
# Access to prometheus is directly through the API # Access to prometheus is directly through the API
prop_accessor :api_url 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 validates :api_url, url: true
end end
before_save :synchronize_service_state!
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
def initialize_properties def initialize_properties
...@@ -20,12 +23,20 @@ class PrometheusService < MonitoringService ...@@ -20,12 +23,20 @@ class PrometheusService < MonitoringService
end end
end end
def show_active_box?
false
end
def editable?
manual_configuration? || !prometheus_installed?
end
def title def title
'Prometheus' 'Prometheus'
end end
def description def description
s_('PrometheusService|Prometheus monitoring') s_('PrometheusService|Time-series monitoring service')
end end
def self.to_param def self.to_param
...@@ -33,7 +44,15 @@ class PrometheusService < MonitoringService ...@@ -33,7 +44,15 @@ class PrometheusService < MonitoringService
end end
def fields def fields
return [] unless editable?
[ [
{
type: 'checkbox',
name: 'manual_configuration',
title: s_('PrometheusService|Active'),
required: true
},
{ {
type: 'text', type: 'text',
name: 'api_url', name: 'api_url',
...@@ -59,7 +78,7 @@ class PrometheusService < MonitoringService ...@@ -59,7 +78,7 @@ class PrometheusService < MonitoringService
end end
def deployment_metrics(deployment) 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) || {} metrics&.merge(deployment_time: deployment.created_at.to_i) || {}
end end
...@@ -68,7 +87,7 @@ class PrometheusService < MonitoringService ...@@ -68,7 +87,7 @@ class PrometheusService < MonitoringService
end end
def additional_deployment_metrics(deployment) 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 end
def matched_metrics def matched_metrics
...@@ -79,6 +98,9 @@ class PrometheusService < MonitoringService ...@@ -79,6 +98,9 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(query_class_name, *args) def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete? 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) data = Kernel.const_get(query_class_name).new(client).query(*args)
{ {
success: true, success: true,
...@@ -89,14 +111,55 @@ class PrometheusService < MonitoringService ...@@ -89,14 +111,55 @@ class PrometheusService < MonitoringService
{ success: false, result: err.message } { success: false, result: err.message }
end end
def client def client(environment_id = nil)
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url) 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 end
private 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) def rename_data_to_metrics(metrics)
metrics[:metrics] = metrics.delete :data metrics[:metrics] = metrics.delete :data
metrics metrics
end end
def synchronize_service_state!
self.active = prometheus_installed? || manual_configuration?
true
end
end end
...@@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper include GitlabRoutingHelper
include MarkupHelper include MarkupHelper
include TreeHelper include TreeHelper
include Gitlab::Utils::StrongMemoize
presents :merge_request presents :merge_request
...@@ -43,7 +44,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -43,7 +44,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def revert_in_fork_path 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 = { continue_params = {
to: merge_request_path(merge_request), to: merge_request_path(merge_request),
notice: "#{edit_in_new_fork_notice} Try to cherry-pick this commit again.", notice: "#{edit_in_new_fork_notice} Try to cherry-pick this commit again.",
...@@ -157,7 +158,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -157,7 +158,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def can_revert_on_current_merge_request? 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 end
def can_cherry_pick_on_current_merge_request? def can_cherry_pick_on_current_merge_request?
...@@ -170,6 +171,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -170,6 +171,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
private private
def cached_can_be_reverted?
strong_memoize(:can_be_reverted) do
can_be_reverted?(current_user)
end
end
def conflicts def conflicts
@conflicts ||= MergeRequests::Conflicts::ListService.new(merge_request) @conflicts ||= MergeRequests::Conflicts::ListService.new(merge_request)
end end
......
...@@ -7,6 +7,8 @@ module Ci ...@@ -7,6 +7,8 @@ module Ci
# stage. # stage.
# #
class EnsureStageService < BaseService class EnsureStageService < BaseService
EnsureStageError = Class.new(StandardError)
def execute(build) def execute(build)
@build = build @build = build
...@@ -22,8 +24,16 @@ module Ci ...@@ -22,8 +24,16 @@ module Ci
private private
def ensure_stage def ensure_stage(attempts: 2)
find_stage || create_stage 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 end
def find_stage def find_stage
......
module Ci module Ci
class RetryBuildService < ::BaseService class RetryBuildService < ::BaseService
CLONE_ACCESSORS = %i[pipeline project ref tag options commands name 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 yaml_variables when environment coverage_regex
description tag_list protected].freeze description tag_list protected].freeze
......
...@@ -9,10 +9,7 @@ module MergeRequests ...@@ -9,10 +9,7 @@ module MergeRequests
merge_request.source_branch = params[:source_branch] merge_request.source_branch = params[:source_branch]
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_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 create(merge_request)
Gitlab::GitalyClient.allow_n_plus_1_calls do
create(merge_request)
end
end end
def before_create(merge_request) def before_create(merge_request)
......
.layout-page{ class: page_with_sidebar_class } .layout-page{ class: page_with_sidebar_class }
- if defined?(nav) && nav - if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}" = render "layouts/nav/sidebar/#{nav}"
.content-wrapper .content-wrapper{ class: "#{@content_wrapper_class}" }
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
.mobile-overlay .mobile-overlay
.alert-wrapper .alert-wrapper
......
...@@ -14,7 +14,8 @@ ...@@ -14,7 +14,8 @@
cluster_status: @cluster.status_name, cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), 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 .js-cluster-application-notice
.flash-container .flash-container
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
= link_to @environment.name, environment_path(@environment) = link_to @environment.name, environment_path(@environment)
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), #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'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
}, },
time: c.time, time: c.time,
space: c.spaces.first, space: c.spaces.first,
refs: get_refs(@graph.repo, c), refs: refs(@graph.repo, c),
id: c.sha, id: c.sha,
date: c.date, date: c.date,
message: c.message, message: c.message,
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
%p= @service.description %p= @service.description
.col-lg-9 .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 = render 'shared/service_settings', form: form, subject: @service
- if @service.editable? - if @service.editable?
.footer-block.row-content-block .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) - link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row %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 .title
= link_to reliable_snippet_path(snippet) do = 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 ...@@ -543,7 +543,7 @@ ActiveRecord::Schema.define(version: 20180206200543) do
t.integer "lock_version" t.integer "lock_version"
end 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", ["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 add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
......
...@@ -664,7 +664,6 @@ Example response: ...@@ -664,7 +664,6 @@ Example response:
] ]
``` ```
## Project Search API ## Project Search API
Search within the specified project. 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 ...@@ -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/). 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 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 ## Performance testing on Review Apps
......
...@@ -102,6 +102,11 @@ running: ...@@ -102,6 +102,11 @@ running:
kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 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 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 preferences and domain provider. But in case you are not sure, just create an
A record with a wildcard host like `*.<your-domain>`. A record with a wildcard host like `*.<your-domain>`.
......
...@@ -134,6 +134,41 @@ added directly to your configured cluster. Those applications are needed for ...@@ -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. | | [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 | | [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 ## Setting the environment scope
When adding more than one clusters, you need to differentiate them with an 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 ...@@ -34,7 +34,7 @@ and deploy from one single platform. Issue Boards help you to visualize
and manage the entire process _in_ GitLab. and manage the entire process _in_ GitLab.
With [Multiple Issue Boards](#multiple-issue-boards), available 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 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, organized from a broader perspective with one Issue Board per project,
but also allow your team members to organize their own workflow by creating 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. ...@@ -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: Visit the GitLab Pages group for a full list of example projects:
<https://gitlab.com/groups/pages>. <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 ### Add a custom domain to your Pages website
For a complete guide on Pages domains, read through the article 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 ...@@ -68,7 +68,7 @@ You can live preview changes submitted to a new branch with
With [GitLab Enterprise Edition](https://about.gitlab.com/products/) With [GitLab Enterprise Edition](https://about.gitlab.com/products/)
subscriptions, you can also request 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: To create, delete, and [branches](branches/index.md) via GitLab's UI:
......
...@@ -6,6 +6,8 @@ module Gitlab ...@@ -6,6 +6,8 @@ module Gitlab
def initialize(config) def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true) @config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e
raise FormatError, e.message
end end
def valid? def valid?
......
...@@ -85,7 +85,7 @@ module Gitlab ...@@ -85,7 +85,7 @@ module Gitlab
begin begin
Gitlab::Ci::YamlProcessor.new(content, opts) Gitlab::Ci::YamlProcessor.new(content, opts)
nil nil
rescue ValidationError, Psych::SyntaxError => e rescue ValidationError => e
e.message e.message
end end
end end
......
...@@ -402,15 +402,6 @@ module Gitlab ...@@ -402,15 +402,6 @@ module Gitlab
end end
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 # Get ref names collection
# #
# Ex. # Ex.
...@@ -418,7 +409,7 @@ module Gitlab ...@@ -418,7 +409,7 @@ module Gitlab
# #
def ref_names(repo) def ref_names(repo)
refs(repo).map do |ref| refs(repo).map do |ref|
ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "") ref.sub(%r{^refs/(heads|remotes|tags)/}, "")
end end
end end
...@@ -553,6 +544,15 @@ module Gitlab ...@@ -553,6 +544,15 @@ module Gitlab
date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i) date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i)
) )
end 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 end
end end
...@@ -631,21 +631,18 @@ module Gitlab ...@@ -631,21 +631,18 @@ module Gitlab
end end
end end
# Get refs hash which key is SHA1 # Get refs hash which key is is the commit id
# and value is a Rugged::Reference # and value is a Gitlab::Git::Tag or Gitlab::Git::Branch
# Note that both inherit from Gitlab::Git::Ref
def refs_hash def refs_hash
# Initialize only when first call return @refs_hash if @refs_hash
if @refs_hash.nil?
@refs_hash = Hash.new { |h, k| h[k] = [] } @refs_hash = Hash.new { |h, k| h[k] = [] }
rugged.references.each do |r| (tags + branches).each do |ref|
# Symbolic/remote references may not have an OID; skip over them next unless ref.target && ref.name
target_oid = r.target.try(:oid)
if target_oid @refs_hash[ref.dereferenced_target.id] << ref.name
sha = rev_parse_target(target_oid).oid
@refs_hash[sha] << r
end
end
end end
@refs_hash @refs_hash
......
...@@ -25,9 +25,8 @@ module Gitlab ...@@ -25,9 +25,8 @@ module Gitlab
@repository.exists? @repository.exists?
end end
# Disabled because of https://gitlab.com/gitlab-org/gitaly/merge_requests/539
def write_page(name, format, content, commit_details) 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 if is_enabled
gitaly_write_page(name, format, content, commit_details) gitaly_write_page(name, format, content, commit_details)
gollum_wiki.clear_cache gollum_wiki.clear_cache
...@@ -48,9 +47,8 @@ module Gitlab ...@@ -48,9 +47,8 @@ module Gitlab
end end
end end
# Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42094
def update_page(page_path, title, format, content, commit_details) 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 if is_enabled
gitaly_update_page(page_path, title, format, content, commit_details) gitaly_update_page(page_path, title, format, content, commit_details)
gollum_wiki.clear_cache gollum_wiki.clear_cache
......
...@@ -222,14 +222,25 @@ module Gitlab ...@@ -222,14 +222,25 @@ module Gitlab
end end
def find_commit(revision) def find_commit(revision)
request = Gitaly::FindCommitRequest.new( if RequestStore.active?
repository: @gitaly_repo, # We don't use RequeStstore.fetch(key) { ... } directly because `revision`
revision: encode_binary(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 = {
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout) storage: @gitaly_repo.storage_name,
relative_path: @gitaly_repo.relative_path,
response.commit 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 end
def patch(revision) def patch(revision)
...@@ -346,6 +357,17 @@ module Gitlab ...@@ -346,6 +357,17 @@ module Gitlab
def encode_repeated(a) def encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } ) Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
end 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 end
end end
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
class AdditionalMetricsDeploymentQuery < BaseQuery class AdditionalMetricsDeploymentQuery < BaseQuery
include QueryAdditionalMetrics include QueryAdditionalMetrics
def query(deployment_id) def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment| Deployment.find_by(id: deployment_id).try do |deployment|
query_metrics( query_metrics(
common_query_context( common_query_context(
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module Prometheus module Prometheus
module Queries module Queries
class DeploymentQuery < BaseQuery class DeploymentQuery < BaseQuery
def query(deployment_id) def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment| Deployment.find_by(id: deployment_id).try do |deployment|
environment_slug = deployment.environment.slug environment_slug = deployment.environment.slug
......
...@@ -3,10 +3,10 @@ module Gitlab ...@@ -3,10 +3,10 @@ module Gitlab
# Helper methods to interact with Prometheus network services & resources # Helper methods to interact with Prometheus network services & resources
class PrometheusClient class PrometheusClient
attr_reader :api_url attr_reader :rest_client, :headers
def initialize(api_url:) def initialize(rest_client)
@api_url = api_url @rest_client = rest_client
end end
def ping def ping
...@@ -40,37 +40,40 @@ module Gitlab ...@@ -40,37 +40,40 @@ module Gitlab
private private
def json_api_get(type, args = {}) 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 rescue Errno::ECONNREFUSED
raise PrometheusError, 'Connection refused' raise PrometheusError, 'Connection refused'
end end
def join_api_url(type, args = {}) def get(path, args)
url = URI.parse(api_url) response = rest_client[path].get(params: args)
rescue URI::Error handle_response(response)
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))
rescue SocketError rescue SocketError
raise PrometheusError, "Can't connect to #{url}" raise PrometheusError, "Can't connect to #{rest_client.url}"
rescue OpenSSL::SSL::SSLError rescue OpenSSL::SSL::SSLError
raise PrometheusError, "#{url} contains invalid SSL data" raise PrometheusError, "#{rest_client.url} contains invalid SSL data"
rescue HTTParty::Error rescue RestClient::ExceptionWithResponse => ex
handle_exception_response(ex.response)
rescue RestClient::Exception
raise PrometheusError, "Network connection error" raise PrometheusError, "Network connection error"
end end
def handle_response(response) def handle_response(response)
if response.code == 200 && response['status'] == 'success' json_data = JSON.parse(response.body)
response['data'] || {} if response.code == 200 && json_data['status'] == 'success'
elsif response.code == 400 json_data['data'] || {}
raise PrometheusError, response['error'] || 'Bad data received' 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 else
raise PrometheusError, "#{response.code} - #{response.body}" raise PrometheusError, "#{response.code} - #{response.body}"
end end
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-07 14:09+0100\n" "POT-Creation-Date: 2018-02-07 13:35+0100\n"
"PO-Revision-Date: 2018-02-07 14:09+0100\n" "PO-Revision-Date: 2018-02-07 13:35+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -2515,7 +2515,7 @@ msgstr "" ...@@ -2515,7 +2515,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "Nessuna metrica è stata monitorata. Per iniziare a monitorare, rilascia ...@@ -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/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2032,7 +2032,7 @@ msgstr "" ...@@ -2032,7 +2032,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "Nenhuma métrica está sendo monitorada. Para inicar o monitoramento, fa ...@@ -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/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "URL da API base do Prometheus. como 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" msgstr "Monitoramento com Prometheus"
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2032,7 +2032,7 @@ msgstr "" ...@@ -2032,7 +2032,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2032,7 +2032,7 @@ msgstr "Жодні метрики не відслідковуються. Для ...@@ -2032,7 +2032,7 @@ msgstr "Жодні метрики не відслідковуються. Для
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Базова адреса Prometheus API, наприклад http://prometheus.example.com/" msgstr "Базова адреса Prometheus API, наприклад http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "Моніторинг Prometheus" msgstr "Моніторинг Prometheus"
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "没有监测指标。要开始监测,请部署到环境中。" ...@@ -2004,7 +2004,7 @@ msgstr "没有监测指标。要开始监测,请部署到环境中。"
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Prometheus API 地址,例如 http://prometheus.example.com/" msgstr "Prometheus API 地址,例如 http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "Prometheus 监测" msgstr "Prometheus 监测"
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -297,7 +297,8 @@ FactoryBot.define do ...@@ -297,7 +297,8 @@ FactoryBot.define do
project.create_prometheus_service( project.create_prometheus_service(
active: true, active: true,
properties: { properties: {
api_url: 'https://prometheus.example.com' api_url: 'https://prometheus.example.com/',
manual_configuration: true
} }
) )
end end
......
...@@ -30,7 +30,8 @@ FactoryBot.define do ...@@ -30,7 +30,8 @@ FactoryBot.define do
project project
active true active true
properties({ properties({
api_url: 'https://prometheus.example.com/' api_url: 'https://prometheus.example.com/',
manual_configuration: true
}) })
end end
......
require 'spec_helper' require 'spec_helper'
# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages describe 'User views a wiki page' do
describe 'User views a wiki page', :skip_gitaly_mock do shared_examples 'wiki page user view' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) } let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_page) do let(:wiki_page) do
create(:wiki_page, create(:wiki_page,
wiki: project.wiki, wiki: project.wiki,
attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' }) attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' })
end end
before do
project.add_master(user)
sign_in(user)
end
context 'when wiki is empty' do
before 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 click_on('New page')
fill_in(:new_wiki_path, with: 'one/two/three-test')
click_on('Create page')
end
page.within('.wiki-form') do page.within('#modal-new-wiki') do
fill_in(:wiki_content, with: 'wiki content') fill_in(:new_wiki_path, with: 'one/two/three-test')
click_on('Create page') click_on('Create page')
end
page.within('.wiki-form') do
fill_in(:wiki_content, with: 'wiki content')
click_on('Create page')
end
end end
end
it 'shows the history of a page that has a path', :js do it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test') expect(current_path).to include('one/two/three-test')
first(:link, text: 'Three').click first(:link, text: 'Three').click
click_on('Page history') 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 page.within(:css, '.nav-text') do
expect(page).to have_content('History') expect(page).to have_content('History')
end
end end
end
it 'shows an old version of a page', :js do it 'shows an old version of a page', :js do
expect(current_path).to include('one/two/three-test') expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three') 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(current_path).to include('one/two/three-test')
expect(page).to have_content('Edit Page') 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('Save changes')
click_on('Page history') click_on('Page history')
page.within(:css, '.nav-text') do page.within(:css, '.nav-text') do
expect(page).to have_content('History') expect(page).to have_content('History')
end end
find('a[href*="?version_id"]') find('a[href*="?version_id"]')
end
end end
end
context 'when a page does not have history' do context 'when a page does not have history' do
before do before do
visit(project_wiki_path(project, wiki_page)) visit(project_wiki_path(project, wiki_page))
end end
it 'shows all the pages' do it 'shows all the pages' do
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
end end
it 'shows a file stored in a page' do it 'shows a file stored in a page' do
gollum_file_double = double('Gollum::File', gollum_file_double = double('Gollum::File',
mime_type: 'image/jpeg', mime_type: 'image/jpeg',
name: 'images/image.jpg', name: 'images/image.jpg',
path: 'images/image.jpg', path: 'images/image.jpg',
raw_data: '') raw_data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg') 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_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_xpath('//img[@data-src="image.jpg"]')
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/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(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 expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end end
it 'shows the creation page if file does not exist' do 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") 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(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page') expect(page).to have_content('New Wiki Page')
expect(page).to have_content('Create page') expect(page).to have_content('Create page')
end
end end
end
context 'when a page has history' do context 'when a page has history' do
before do before do
wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)')
end end
it 'shows the page history' do it 'shows the page history' do
visit(project_wiki_path(project, wiki_page)) 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.name)
expect(page).to have_content("#{user.username} created page: home") expect(page).to have_content("#{user.username} created page: home")
expect(page).to have_content('updated 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 end
it 'does not show the "Edit" button' do it 'opens a default wiki page', :js do
visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) 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
end end
it 'opens a default wiki page', :js do context 'when Gitaly is enabled' do
visit(project_path(project)) it_behaves_like 'wiki page user view'
end
find('.shortcuts-wiki').click
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
end end
...@@ -7,10 +7,10 @@ describe GraphHelper do ...@@ -7,10 +7,10 @@ describe GraphHelper do
let(:graph) { Network::Graph.new(project, 'master', commit, '') } let(:graph) { Network::Graph.new(project, 'master', commit, '') }
it 'filters our refs used by GitLab' do 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) self.instance_variable_set(:@graph, graph)
refs = get_refs(project.repository, commit) refs = refs(project.repository, commit)
expect(refs).to eq('master')
expect(refs).to match('master')
end end
end end
end end
...@@ -105,4 +105,68 @@ describe('dateInWords', () => { ...@@ -105,4 +105,68 @@ describe('dateInWords', () => {
it('should return abbreviated month name', () => { it('should return abbreviated month name', () => {
expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); 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', () => { ...@@ -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>'); 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', () => { ...@@ -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 .'); 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', () => { ...@@ -29,34 +29,6 @@ describe('EmptyState', () => {
expect(component.currentState).toBe(component.states.gettingStarted); 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', () => { it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
const component = createComponent({ const component = createComponent({
selectedState: 'unableToConnect', selectedState: 'unableToConnect',
...@@ -88,6 +60,7 @@ describe('EmptyState', () => { ...@@ -88,6 +60,7 @@ describe('EmptyState', () => {
const component = createComponent({ const component = createComponent({
selectedState: 'gettingStarted', selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath, settingsPath: statePaths.settingsPath,
clustersPath: statePaths.clustersPath,
documentationPath: statePaths.documentationPath, documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo', emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo', emptyLoadingSvgPath: 'foo',
......
...@@ -2471,6 +2471,7 @@ export const deploymentData = [ ...@@ -2471,6 +2471,7 @@ export const deploymentData = [
export const statePaths = { export const statePaths = {
settingsPath: '/root/hello-prometheus/services/prometheus/edit', settingsPath: '/root/hello-prometheus/services/prometheus/edit',
clustersPath: '/root/hello-prometheus/clusters',
documentationPath: '/help/administration/monitoring/prometheus/index.md', documentationPath: '/help/administration/monitoring/prometheus/index.md',
}; };
......
...@@ -247,5 +247,44 @@ describe Gitlab::Checks::ChangeAccess do ...@@ -247,5 +247,44 @@ describe Gitlab::Checks::ChangeAccess do
end end
end 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
end end
...@@ -38,6 +38,16 @@ describe Gitlab::Ci::Config::Loader do ...@@ -38,6 +38,16 @@ describe Gitlab::Ci::Config::Loader do
end end
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 context 'when yaml config is empty' do
let(:yml) { '' } let(:yml) { '' }
......
...@@ -1394,11 +1394,15 @@ EOT ...@@ -1394,11 +1394,15 @@ EOT
describe "Error handling" do describe "Error handling" do
it "fails to parse YAML" 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 end
it "indicates that object is invalid" do 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 end
it "returns errors if tags parameter is invalid" do it "returns errors if tags parameter is invalid" do
...@@ -1688,37 +1692,36 @@ EOT ...@@ -1688,37 +1692,36 @@ EOT
end end
describe "#validation_message" do describe "#validation_message" do
subject { Gitlab::Ci::YamlProcessor.validation_message(content) }
context "when the YAML could not be parsed" do context "when the YAML could not be parsed" do
it "returns an error about invalid configutaion" do let(:content) { YAML.dump("invalid: yaml: test") }
content = YAML.dump("invalid: yaml: test")
expect(Gitlab::Ci::YamlProcessor.validation_message(content)) it { is_expected.to eq "Invalid configuration format" }
.to eq "Invalid configuration format"
end
end end
context "when the tags parameter is invalid" do context "when the tags parameter is invalid" do
it "returns an error about invalid tags" do let(:content) { YAML.dump({ rspec: { script: "test", tags: "mysql" } }) }
content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect(Gitlab::Ci::YamlProcessor.validation_message(content)) it { is_expected.to eq "jobs:rspec tags should be an array of strings" }
.to eq "jobs:rspec tags should be an array of strings"
end
end end
context "when YAML content is empty" do context "when YAML content is empty" do
it "returns an error about missing content" do let(:content) { '' }
expect(Gitlab::Ci::YamlProcessor.validation_message(''))
.to eq "Please provide content of .gitlab-ci.yml" it { is_expected.to eq "Please provide content of .gitlab-ci.yml" }
end 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 end
context "when the YAML is valid" do context "when the YAML is valid" do
it "does not return any errors" do let(:content) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil it { is_expected.to be_nil }
end
end end
end end
......
...@@ -600,12 +600,16 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -600,12 +600,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
describe "#refs_hash" do describe "#refs_hash" do
let(:refs) { repository.refs_hash } subject { repository.refs_hash }
it "should have as many entries as branches and tags" do it "should have as many entries as branches and tags" do
expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS 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 # 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
end end
......
...@@ -3,34 +3,38 @@ require 'spec_helper' ...@@ -3,34 +3,38 @@ require 'spec_helper'
describe Gitlab::Git::Wiki do describe Gitlab::Git::Wiki do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { project.owner } let(:user) { project.owner }
let(:wiki) { ProjectWiki.new(project, user) } let(:project_wiki) { ProjectWiki.new(project, user) }
let(:gollum_wiki) { wiki.wiki } subject { project_wiki.wiki }
# Remove skip_gitaly_mock flag when gitaly_find_page when # 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 describe '#page', :skip_gitaly_mock do
it 'returns the right page' do before do
create_page('page1', 'content') create_page('page1', 'content')
create_page('foo/page1', 'content') create_page('foo/page1', 'content foo/page1')
end
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'
after do
destroy_page('page1') destroy_page('page1')
destroy_page('page1', 'foo') destroy_page('page1', 'foo')
end 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 end
def create_page(name, content) def create_page(name, content)
gollum_wiki.write_page(name, :markdown, content, commit_details) subject.write_page(name, :markdown, content, commit_details(name))
end end
def commit_details def commit_details(name)
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}")
end end
def destroy_page(title, dir = '') def destroy_page(title, dir = '')
page = gollum_wiki.page(title: title, dir: dir) page = subject.page(title: title, dir: dir)
wiki.delete_page(page, "test commit") project_wiki.delete_page(page, "test commit")
end end
end end
...@@ -166,6 +166,32 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -166,6 +166,32 @@ describe Gitlab::GitalyClient::CommitService do
described_class.new(repository).find_commit(revision) described_class.new(repository).find_commit(revision)
end 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 end
describe '#patch' do describe '#patch' do
......
...@@ -7,7 +7,7 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do ...@@ -7,7 +7,7 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do
include_examples 'additional metrics query' do include_examples 'additional metrics query' do
let(:deployment) { create(:deployment, environment: environment) } let(:deployment) { create(:deployment, environment: environment) }
let(:query_params) { [deployment.id] } let(:query_params) { [environment.id, deployment.id] }
it 'queries using specific time' do it 'queries using specific time' do
expect(client).to receive(:query_range).with(anything, expect(client).to receive(:query_range).with(anything,
......
...@@ -31,7 +31,7 @@ describe Gitlab::Prometheus::Queries::DeploymentQuery do ...@@ -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', expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
time: stop_time) time: stop_time)
expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_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) cpu_values: nil, cpu_before: nil, cpu_after: nil)
end end
end end
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::PrometheusClient do describe Gitlab::PrometheusClient do
include PrometheusHelpers 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 describe '#ping' do
it 'issues a "query" request to the API endpoint' do it 'issues a "query" request to the API endpoint' do
...@@ -47,16 +47,28 @@ describe Gitlab::PrometheusClient do ...@@ -47,16 +47,28 @@ describe Gitlab::PrometheusClient do
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
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 end
describe 'failure to reach a provided prometheus url' do describe 'failure to reach a provided prometheus url' do
let(:prometheus_url) {"https://prometheus.invalid.example.com"} let(:prometheus_url) {"https://prometheus.invalid.example.com"}
subject { described_class.new(RestClient::Resource.new(prometheus_url)) }
context 'exceptions are raised' do context 'exceptions are raised' do
it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) 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}") .to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
...@@ -64,15 +76,15 @@ describe Gitlab::PrometheusClient do ...@@ -64,15 +76,15 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' 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) 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") .to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
it 'raises a Gitlab::PrometheusError error when a HTTParty::Error is rescued' do it 'raises a Gitlab::PrometheusError error when a RestClient::Exception is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, HTTParty::Error) 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") .to raise_error(Gitlab::PrometheusError, "Network connection error")
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end 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 ...@@ -6,6 +6,24 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application specs', described_class 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 describe "#chart_values_file" do
subject { create(:clusters_applications_prometheus).chart_values_file } subject { create(:clusters_applications_prometheus).chart_values_file }
...@@ -13,4 +31,58 @@ describe Clusters::Applications::Prometheus do ...@@ -13,4 +31,58 @@ describe Clusters::Applications::Prometheus do
expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml") expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
end end
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 end
...@@ -2471,7 +2471,7 @@ describe Project do ...@@ -2471,7 +2471,7 @@ describe Project do
create(:ci_variable, :protected, value: 'protected', project: project) create(:ci_variable, :protected, value: 'protected', project: project)
end end
subject { project.secret_variables_for(ref: 'ref') } subject { project.reload.secret_variables_for(ref: 'ref') }
before do before do
stub_application_setting( 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 ...@@ -5,7 +5,11 @@ describe Ci::RetryBuildService do
set(:project) { create(:project) } set(:project) { create(:project) }
set(:pipeline) { create(:ci_pipeline, project: 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 let(:service) do
described_class.new(project, user) described_class.new(project, user)
...@@ -28,29 +32,27 @@ describe Ci::RetryBuildService do ...@@ -28,29 +32,27 @@ describe Ci::RetryBuildService do
sourced_pipelines artifacts_file_store artifacts_metadata_store].freeze # EE sourced_pipelines artifacts_file_store artifacts_metadata_store].freeze # EE
shared_examples 'build duplication' do shared_examples 'build duplication' do
let(:stage) do let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
# 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(:build) do let(:build) do
create(:ci_build, :failed, :artifacts, :expired, :erased, create(:ci_build, :failed, :artifacts, :expired, :erased,
:queued, :coverage, :tags, :allowed_to_fail, :on_tag, :queued, :coverage, :tags, :allowed_to_fail, :on_tag,
:triggered, :trace_artifact, :teardown_environment, :triggered, :trace_artifact, :teardown_environment,
description: 'my-job', stage: 'test', pipeline: pipeline, description: 'my-job', stage: 'test', stage_id: stage.id,
auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build| pipeline: pipeline, auto_canceled_by: another_pipeline)
## end
# TODO, workaround for FactoryBot limitation when having both
# stage (text) and stage_id (integer) columns in the table. before do
build.stage_id = stage.id # Make sure that build has both `stage_id` and `stage` because FactoryBot
end # 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 end
describe 'clone accessors' do describe 'clone accessors' do
CLONE_ACCESSORS.each do |attribute| CLONE_ACCESSORS.each do |attribute|
it "clones #{attribute} build attribute" do 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)).not_to be_nil
expect(new_build.send(attribute)).to eq build.send(attribute) expect(new_build.send(attribute)).to eq build.send(attribute)
end end
...@@ -123,10 +125,12 @@ describe Ci::RetryBuildService do ...@@ -123,10 +125,12 @@ describe Ci::RetryBuildService do
context 'when there are subsequent builds that are skipped' do context 'when there are subsequent builds that are skipped' do
let!(:subsequent_build) 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 end
it 'resumes pipeline processing in subsequent stages' do it 'resumes pipeline processing in a subsequent stage' do
service.execute(build) service.execute(build)
expect(subsequent_build.reload).to be_created 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