Commit 3c6ad577 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into 'webpack-mirrors'

# Conflicts:
#   config/webpack.config.js
parents eaab051b 68a13836
......@@ -37,10 +37,11 @@ export default class Clusters {
clusterStatusReason,
helpPath,
ingressHelpPath,
ingressDnsHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
......@@ -98,6 +99,7 @@ export default class Clusters {
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
},
});
},
......
......@@ -36,10 +36,6 @@
type: String,
required: false,
},
description: {
type: String,
required: true,
},
status: {
type: String,
required: false,
......@@ -148,7 +144,7 @@
class="table-section section-wrap"
role="gridcell"
>
<div v-html="description"></div>
<slot name="description"></slot>
</div>
<div
class="table-section table-button-footer section-align-top"
......
......@@ -2,10 +2,16 @@
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import {
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
export default {
components: {
applicationRow,
clipboardButton,
},
props: {
applications: {
......@@ -23,6 +29,11 @@
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
......@@ -43,19 +54,16 @@
false,
);
},
helmTillerDescription() {
return _.escape(s__(
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const descriptionParagraph = _.escape(s__(
`ClusterIntegration|Ingress gives you a way to route requests to services based on the
request host or path, centralizing a number of services into a single entrypoint.`,
));
const extraCostParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
......@@ -83,9 +91,6 @@
);
return `
<p>
${descriptionParagraph}
</p>
<p>
${extraCostParagraph}
</p>
......@@ -136,33 +141,121 @@
id="helm"
:title="applications.helm.title"
title-link="https://docs.helm.sh/"
:description="helmTillerDescription"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
/>
>
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) }}
</div>
</application-row>
<application-row
id="ingress"
:id="ingressId"
:title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:description="ingressDescription"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
>
<div slot="description">
<p>
{{ s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`) }}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }}
</label>
<div
v-if="ingressExternalIp"
class="input-group"
>
<input
type="text"
id="ingress-ip-address"
class="form-control js-ip-address"
:value="ingressExternalIp"
readonly
/>
<span class="input-group-btn">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
css-class="btn btn-default js-clipboard-btn"
/>
</span>
</div>
<input
v-else
type="text"
class="form-control js-ip-address"
readonly
value="?"
/>
</div>
<p
v-if="!ingressExternalIp"
class="settings-message js-no-ip-message"
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
<a
:href="ingressHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
<div
v-else
v-html="ingressDescription"
>
</div>
</div>
</application-row>
<application-row
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
/>
>
<div
slot="description"
v-html="prometheusDescription"
>
</div>
</application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
......
......@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored';
export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
import { s__ } from '../../locale';
import { INGRESS } from '../constants';
export default class ClusterStore {
constructor() {
......@@ -21,6 +22,7 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
externalIp: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
......@@ -40,9 +42,10 @@ export default class ClusterStore {
};
}
setHelpPaths(helpPath, ingressHelpPath) {
setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
this.state.ingressDnsHelpPath = ingressDnsHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
......@@ -64,6 +67,7 @@ export default class ClusterStore {
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
......@@ -76,6 +80,10 @@ export default class ClusterStore {
status,
statusReason,
};
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
}
});
}
}
import initRegistryImages from '~/registry/index';
document.addEventListener('DOMContentLoaded', initRegistryImages);
......@@ -4,10 +4,14 @@ import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
});
/* eslint-disable no-unused-vars */
import ProtectedBranchCreate from './protected_branch_create';
import ProtectedBranchEditList from './protected_branch_edit_list';
$(() => {
const protectedBranchCreate = new ProtectedBranchCreate();
const protectedBranchEditList = new ProtectedBranchEditList();
});
......@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: '#js-vue-registry-images',
components: {
registryApp,
......@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});
/* eslint-disable func-names, wrap-iife */
/* global u2f */
import _ from 'underscore';
import isU2FSupported from './util';
import importU2FLibrary from './util';
import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
......@@ -10,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup
export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.u2fUtils = null;
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this);
......@@ -50,22 +49,23 @@ export default class U2FAuthenticate {
}
start() {
if (isU2FSupported()) {
return this.renderInProgress();
}
return this.renderNotSupported();
return importU2FLibrary()
.then((utils) => {
this.u2fUtils = utils;
this.renderInProgress();
})
.catch(() => this.renderNotSupported());
}
authenticate() {
return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) {
return function (response) {
return this.u2fUtils.sign(this.appId, this.challenge, this.signRequests,
(response) => {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error);
return this.renderError(error);
}
return _this.renderAuthenticated(JSON.stringify(response));
};
})(this), 10);
return this.renderAuthenticated(JSON.stringify(response));
}, 10);
}
renderTemplate(name, params) {
......
/* eslint-disable func-names, wrap-iife */
/* global u2f */
import _ from 'underscore';
import isU2FSupported from './util';
import importU2FLibrary from './util';
import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
......@@ -11,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup
export default class U2FRegister {
constructor(container, u2fParams) {
this.u2fUtils = null;
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderRegistered = this.renderRegistered.bind(this);
......@@ -34,22 +32,23 @@ export default class U2FRegister {
}
start() {
if (isU2FSupported()) {
return this.renderSetup();
}
return this.renderNotSupported();
return importU2FLibrary()
.then((utils) => {
this.u2fUtils = utils;
this.renderSetup();
})
.catch(() => this.renderNotSupported());
}
register() {
return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) {
return function (response) {
return this.u2fUtils.register(this.appId, this.registerRequests, this.signRequests,
(response) => {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
return this.renderError(error);
}
return _this.renderRegistered(JSON.stringify(response));
};
})(this), 10);
return this.renderRegistered(JSON.stringify(response));
}, 10);
}
renderTemplate(name, params) {
......
export default function isU2FSupported() {
return window.u2f;
function isOpera(userAgent) {
return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0;
}
function getOperaVersion(userAgent) {
const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/);
return match ? parseInt(match[1], 10) : false;
}
function isChrome(userAgent) {
return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent);
}
function getChromeVersion(userAgent) {
const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
return match ? parseInt(match[1], 10) : false;
}
export function canInjectU2fApi(userAgent) {
const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41;
const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40;
const isMobile = (
userAgent.indexOf('droid') >= 0 ||
userAgent.indexOf('CriOS') >= 0 ||
/\b(iPad|iPhone|iPod)(?=;)/.test(userAgent)
);
return (isSupportedChrome || isSupportedOpera) && !isMobile;
}
export default function importU2FLibrary() {
if (window.u2f) {
return Promise.resolve(window.u2f);
}
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) {
return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f);
}
return Promise.reject();
}
......@@ -28,6 +28,11 @@
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn btn-default btn-transparent btn-clipboard',
},
},
};
</script>
......@@ -35,7 +40,7 @@
<template>
<button
type="button"
class="btn btn-transparent btn-clipboard"
:class="cssClass"
:title="title"
:data-clipboard-text="text"
v-tooltip
......
......@@ -17,7 +17,7 @@ module IssuableCollections
set_pagination
return if redirect_out_of_range(@total_pages)
if params[:label_name].present?
if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
......
......@@ -14,12 +14,13 @@ class Groups::LabelsController < Groups::ApplicationController
end
format.json do
available_labels =
if params[:only_group_labels]
group.labels
else
LabelsFinder.new(current_user, group_id: @group.id).execute
end
available_labels = LabelsFinder.new(
current_user,
group_id: @group.id,
only_group_labels: params[:only_group_labels],
include_ancestor_groups: params[:include_ancestor_groups],
include_descendant_groups: params[:include_descendant_groups]
).execute
render json: LabelSerializer.new.represent_appearance(available_labels)
end
......
......@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
STATUS_POLLING_INTERVAL = 10_000
......@@ -101,4 +102,8 @@ class Projects::ClustersController < Projects::ApplicationController
def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster)
end
def update_applications_status
@cluster.applications.each(&:schedule_status_update)
end
end
......@@ -61,10 +61,18 @@ class LabelsFinder < UnionFinder
def group_ids
strong_memoize(:group_ids) do
group = Group.find(params[:group_id])
groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group]
groups_user_can_read_labels(groups).map(&:id)
groups_user_can_read_labels(groups_to_include).map(&:id)
end
end
def groups_to_include
group = Group.find(params[:group_id])
groups = [group]
groups += group.ancestors if params[:include_ancestor_groups].present?
groups += group.descendants if params[:include_descendant_groups].present?
groups
end
def group?
......
module U2fHelper
def inject_u2f_api?
((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile?
end
end
......@@ -5,6 +5,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
include AfterCommitQueue
default_value_for :ingress_type, :nginx
default_value_for :version, :nginx
......@@ -13,6 +14,17 @@ module Clusters
nginx: 1
}
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
before_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
end
end
end
def chart
'stable/nginx-ingress'
end
......@@ -24,6 +36,13 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end
def schedule_status_update
return unless installed?
return if external_ip
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
end
end
end
......@@ -23,6 +23,11 @@ module Clusters
def name
self.class.application_name
end
def schedule_status_update
# Override if you need extra data synchronized
# from K8s after installation
end
end
end
end
......
......@@ -6,6 +6,12 @@ class CycleAnalytics
@options = options
end
def all_medians_per_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].median
end
end
def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
from: @options[:from],
......
......@@ -597,15 +597,7 @@ class Repository
def license_key
return unless exists?
# The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
begin
Licensee.license(path).try(:key)
# Normally we would rescue Rugged::Error, but that is banned by lint-rugged
# and we need to migrate this endpoint to Gitaly:
# https://gitlab.com/gitlab-org/gitaly/issues/1026
rescue
end
raw_repository.license_short_name
end
cache_method :license_key
......
......@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :description
expose :median, as: :value do |stage|
stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
# median returns a BatchLoader instance which we first have to unwrap by using to_i
!stage.median.to_i.zero? ? distance_of_time_in_words(stage.median) : nil
end
end
......@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :name
expose :status_name, as: :status
expose :status_reason
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
end
module Clusters
module Applications
class CheckIngressIpAddressService < BaseHelmService
include Gitlab::Utils::StrongMemoize
Error = Class.new(StandardError)
LEASE_TIMEOUT = 15.seconds.to_i
def execute
return if app.external_ip
return unless try_obtain_lease
app.update!(external_ip: ingress_ip) if ingress_ip
end
private
def try_obtain_lease
Gitlab::ExclusiveLease
.new("check_ingress_ip_address_service:#{app.id}", timeout: LEASE_TIMEOUT)
.try_obtain
end
def ingress_ip
service.status.loadBalancer.ingress&.first&.ip
end
def service
strong_memoize(:ingress_service) do
kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
end
end
end
end
end
- if inject_u2f_api?
- content_for :page_specific_javascripts do
= webpack_bundle_tag('u2f')
%div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
.login-box
......
......@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'issues'
- if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
......
......@@ -4,8 +4,6 @@
- content_for :page_specific_javascripts do
- if inject_u2f_api?
= webpack_bundle_tag('u2f')
= webpack_bundle_tag('two_factor_auth')
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
......
......@@ -18,7 +18,14 @@
.email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
= clipboard_button(target: '#issuable_email')
= clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs')
= mail_to email, class: 'btn btn-clipboard btn-transparent',
subject: _("Enter the #{name} title"),
body: _("Enter the #{name} description"),
title: _('Send email'),
data: { toggle: 'tooltip', placement: 'bottom' } do
= sprite_icon('mail')
%p
= render 'by_email_description'
%p
......
......@@ -15,6 +15,7 @@
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice
......
......@@ -6,7 +6,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'issues'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
......
......@@ -7,9 +7,6 @@
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable')
.detail-page-header
.detail-page-header-body
......
......@@ -5,14 +5,8 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable')
- if has_vue_discussions_cookie?
= webpack_bundle_tag('mr_notes')
- if has_vue_discussions_cookie?
- if has_vue_discussions_cookie?
- content_for :page_specific_javascripts do
= webpack_bundle_tag('mr_notes')
.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('protected_branches')
- content_for :create_protected_branch do
= render 'projects/protected_branches/create_protected_branch'
......
......@@ -15,7 +15,6 @@
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('registry_list')
.row.prepend-top-10
.col-lg-12
......
......@@ -24,6 +24,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:check_gcp_project_billing
- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage
- github_importer:github_import_import_diff_note
......
class ClusterWaitForIngressIpAddressWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckIngressIpAddressService.new(app).execute
end
end
end
---
title: Add email button to new issue by email
merge_request: 10942
author: Islam Wazery
---
title: Include cycle time in usage ping data
merge_request: 16973
author:
type: added
---
title: Display ingress IP address in the Kubernetes page
merge_request: 17052
author:
type: added
......@@ -50,8 +50,6 @@ function generateEntries() {
const manualEntries = {
monitoring: './monitoring/monitoring_bundle.js',
mr_notes: './mr_notes/index.js',
protected_branches: './protected_branches',
registry_list: './registry/index.js',
terminal: './terminal/terminal_bundle.js',
two_factor_auth: './two_factor_auth.js',
......@@ -62,21 +60,14 @@ function generateEntries() {
ide: './ide/index.js',
raven: './raven/index.js',
test: './test.js',
u2f: ['vendor/u2f'],
webpack_runtime: './webpack.js',
// EE-only
add_gitlab_slack_application: 'ee/add_gitlab_slack_application/index.js',
burndown_chart: 'ee/burndown_chart/index.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
geo_nodes: 'ee/geo_nodes',
issuable: 'ee/issuable/issuable_bundle.js',
issues: 'ee/issues/issues_bundle.js',
ldap_group_links: 'ee/groups/ldap_group_links.js',
ee_protected_branches: 'ee/protected_branches',
service_desk: 'ee/projects/settings_service_desk/service_desk_bundle.js',
roadmap: 'ee/roadmap',
};
return Object.assign(manualEntries, autoEntries);
......
class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :clusters_applications_ingress, :external_ip, :string
end
end
......@@ -665,6 +665,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do
t.string "version", null: false
t.string "cluster_ip"
t.text "status_reason"
t.string "external_ip"
end
create_table "clusters_applications_prometheus", force: :cascade do |t|
......
......@@ -454,9 +454,85 @@ wal_keep_segments = 10
hot_standby = on
```
#### Tracking Database for the Secondary nodes
NOTE: **Note**:
You only need to follow the steps below if you are not using the managed
PostgreSQL from a Omnibus GitLab package.
Geo secondary nodes use a tracking database to keep track of replication
status and recover automatically from some replication issues. Follow the
instructions for [enabling tracking database on the secondary server][tracking].
status and recover automatically from some replication issues.
This is a separate PostgreSQL installation that can be configured to use
FDW to connect with the secondary database for improved performance.
To enable an external PostgreSQL instance as tracking database, follow
the instructions below:
1. Edit `/etc/gitlab/gitlab.rb` with the connection params and credentials
```ruby
# note this is shared between both databases,
# make sure you define the same password in both
gitlab_rails['db_password'] = 'mypassword'
geo_secondary['db_host'] = '2.3.4.5' # change to the correct public IP
geo_secondary['db_port'] = 5431 # change to the correct port
geo_secondary['db_fdw'] = true # enable FDW
geo_postgresql['enable'] = false # don't use internal managed instance
```
1. Reconfigure GitLab for the changes to take effect:
```bash
gitlab-ctl reconfigure
```
1. Run the tracking database migrations:
```bash
gitlab-rake geo:db:migrate
```
1. Configure the [PostgreSQL FDW][FDW] connection and credentials:
Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
params to match your environment. Execute it to setup the FDW connection.
```bash
#!/bin/bash
# Secondary Database connection params:
DB_HOST="5.6.7.8" # change to the public IP or VPC private IP
DB_NAME="gitlabhq_production"
DB_USER="gitlab"
DB_PORT="5432"
# Tracking Database connection params:
GEO_DB_HOST="2.3.4.5" # change to the public IP or VPC private IP
GEO_DB_NAME="gitlabhq_geo_production"
GEO_DB_USER="gitlab_geo"
GEO_DB_PORT="5432"
query_exec () {
gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
}
query_exec "CREATE EXTENSION postgres_fdw;"
query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}');"
query_exec "CREATE SCHEMA gitlab_secondary;"
query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
```
NOTE: **Note:** The script template above uses `gitlab-psql` as it's intended to be executed from the Geo machine,
but you can change it to `psql` and run it from any machine that has access to the database.
1. Restart GitLab
```bash
gitlab-ctl restart
```
## MySQL replication
......
......@@ -262,28 +262,32 @@ node.
1. Configure the [PostgreSQL FDW][FDW] connection and credentials:
Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
params to match your environment.
params to match your environment. Execute it to setup the FDW connection.
```bash
#!/bin/bash
# Secondary Database connection params:
DB_HOST="/var/opt/gitlab/postgresql"
DB_HOST="/var/opt/gitlab/postgresql" # change to the public IP or VPC private IP if its an external server
DB_NAME="gitlabhq_production"
DB_USER="gitlab"
DB_PORT="5432"
# Tracking Database connection params:
GEO_DB_HOST="/var/opt/gitlab/geo-postgresql"
GEO_DB_HOST="/var/opt/gitlab/geo-postgresql" # change to the public IP or VPC private IP if its an external server
GEO_DB_NAME="gitlabhq_geo_production"
GEO_DB_USER="gitlab_geo"
GEO_DB_PORT="5432"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE EXTENSION postgres_fdw;"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$(DB_HOST)', dbname '$(DB_NAME)', port '$(DB_PORT)' );"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE USER MAPPING FOR $(GEO_DB_USER) SERVER gitlab_secondary OPTIONS (user '$(DB_USER)');"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE SCHEMA gitlab_secondary;"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO $(GEO_DB_USER);"
query_exec () {
gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
}
query_exec "CREATE EXTENSION postgres_fdw;"
query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}');"
query_exec "CREATE SCHEMA gitlab_secondary;"
query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
```
And edit the content of `database_geo.yml` and to add `fdw: true` to
......
......@@ -508,7 +508,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import * as mutations from './mutations';
import mutations from './mutations';
Vue.use(Vuex);
......@@ -527,7 +527,7 @@ _Note:_ If the state of the application is too complex, an individual file for t
An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
```javascript
import * as types from './mutation-types'
import * as types from './mutation_types';
export const addUser = ({ commit }, user) => {
commit(types.ADD_USER, user);
......@@ -577,7 +577,8 @@ import { mapGetters } from 'vuex';
The only way to actually change state in a Vuex store is by committing a mutation.
```javascript
import * as types from './mutation-types'
import * as types from './mutation_types';
export default {
[types.ADD_USER](state, user) {
state.users.push(user);
......@@ -686,4 +687,3 @@ describe('component', () => {
[vuex-testing]: https://vuex.vuejs.org/en/testing.html
[axios]: https://github.com/axios/axios
[axios-interceptors]: https://github.com/axios/axios#interceptors
<script>
/* eslint-disable vue/require-default-prop */
import issuableApp from '~/issue_show/components/app.vue';
import relatedIssuesRoot from 'ee/issuable/related_issues/components/related_issues_root.vue';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import issuableAppEventHub from '~/issue_show/event_hub';
import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
......
import Vue from 'vue';
import EpicShowApp from './components/epic_show_app.vue';
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const el = document.querySelector('#epic-show-app');
const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial);
......@@ -21,4 +21,4 @@ document.addEventListener('DOMContentLoaded', () => {
props,
}),
});
});
};
import Vue from 'vue';
import NewEpicApp from './components/new_epic.vue';
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const el = document.querySelector('#new-epic-app');
if (el) {
const props = el.dataset;
return new Vue({
new Vue({ // eslint-disable-line no-new
el,
components: {
'new-epic-app': NewEpicApp,
......@@ -14,4 +16,5 @@ document.addEventListener('DOMContentLoaded', () => {
props,
}),
});
});
}
};
/* eslint-disable no-new */
$(() => {
class ExportCSVModal {
constructor() {
this.$modal = $('.issues-export-modal');
this.$downloadBtn = $('.csv_download_link');
this.$closeBtn = $('.modal-header .close');
this.init();
}
init() {
this.$modal.modal({ show: false });
this.$downloadBtn.on('click', () => this.$modal.modal('show'));
this.$closeBtn.on('click', () => this.$modal.modal('hide'));
}
}
new ExportCSVModal();
});
import initFilteredSearch from '~/pages/search/init_filtered_search';
import FilteredSearchTokenKeysEpics from 'ee/filtered_search/filtered_search_token_keys_epics';
import initNewEpic from 'ee/epics/new_epic/new_epic_bundle';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
......@@ -7,4 +8,5 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: FilteredSearchTokenKeysEpics,
stateFiltersSelector: '.epics-state-filters',
});
initNewEpic();
});
import ZenMode from '~/zen_mode';
import initEpicShow from 'ee/epics/epic_show/epic_show_bundle';
document.addEventListener('DOMContentLoaded', () => new ZenMode());
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
initEpicShow();
});
import initNewEpic from 'ee/epics/new_epic/new_epic_bundle';
import initRoadmap from 'ee/roadmap/index';
document.addEventListener('DOMContentLoaded', () => {
initNewEpic();
initRoadmap();
});
export default function initExportCSVModal() {
const $modal = $('.issues-export-modal');
const $downloadBtn = $('.csv_download_link');
const $closeBtn = $('.modal-header .close');
$modal.modal({ show: false });
$downloadBtn.on('click', () => $modal.modal('show'));
$closeBtn.on('click', () => $modal.modal('hide'));
}
import '~/pages/projects/issues/index/index';
import initExportCSVModal from './export_csv_modal';
document.addEventListener('DOMContentLoaded', initExportCSVModal);
import initShow from '~/pages/projects/issues/show';
import initSidebarBundle from 'ee/sidebar/sidebar_bundle';
import initRelatedIssues from 'ee/related_issues';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initSidebarBundle();
initRelatedIssues();
});
......@@ -3,21 +3,34 @@ import UsersSelect from '~/users_select';
import UserCallout from '~/user_callout';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create';
import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list';
import CEProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import CEProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import CEProtectedTagCreate from '~/protected_tags/protected_tag_create';
import CEProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import ProtectedBranchCreate from 'ee/protected_branches/protected_branch_create';
import ProtectedBranchEditList from 'ee/protected_branches/protected_branch_edit_list';
import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create';
import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect();
new UserCallout();
initDeployKeys();
initSettingsPanels();
if (document.querySelector('.js-protected-refs-for-users')) {
new ProtectedBranchCreate();
new ProtectedBranchEditList();
new ProtectedTagCreate();
new ProtectedTagEditList();
} else {
new CEProtectedBranchCreate();
new CEProtectedBranchEditList();
new CEProtectedTagCreate();
new CEProtectedTagEditList();
}
initDeployKeys();
initSettingsPanels();
});
import Vue from 'vue';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import RelatedIssuesRoot from './related_issues/components/related_issues_root.vue';
import RelatedIssuesRoot from './components/related_issues_root.vue';
document.addEventListener('DOMContentLoaded', () => {
export default function initRelatedIssues() {
const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
if (relatedIssuesRootElement) {
// eslint-disable-next-line no-new
......@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => {
}),
});
}
});
}
......@@ -13,7 +13,7 @@ import roadmapApp from './components/app.vue';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const el = document.getElementById('js-roadmap');
if (!el) {
......@@ -57,4 +57,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
});
};
......@@ -25,7 +25,10 @@ function mountWeightComponent(mediator) {
function mountEpic() {
const el = document.querySelector('#js-vue-sidebar-item-epic');
return new Vue({
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarItemEpic,
......
......@@ -4,8 +4,6 @@
= render 'shared/issuable/epic_nav', type: :epics
.nav-controls
- if can?(current_user, :create_epic, @group)
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'new_epic'
#new-epic-app{ data: { endpoint: request.url, 'align-right' => true } }
= render 'shared/epic/search_bar', type: :epics
......
......@@ -14,6 +14,5 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'epic_show'
#epic-show-app{ data: { initial: issuable_initial_data(@epic).to_json, meta: epic_meta_data } }
......@@ -7,7 +7,6 @@
- if @epics_count != 0
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'roadmap'
#js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, empty_state_illustration: image_path('illustrations/epics/roadmap.svg') } }
- else
= render 'shared/empty_states/roadmap'
......@@ -3,10 +3,6 @@
- page_title "Service Desk"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'issues'
- content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'ee_protected_branches'
- content_for :create_protected_branch do
= render 'projects/protected_branches/ee/create_protected_branch'
......
......@@ -11,5 +11,4 @@
- if can?(current_user, :create_epic, @group)
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'new_epic'
#new-epic-app{ data: { endpoint: request.url } }
......@@ -11,7 +11,6 @@
- if can?(current_user, :create_epic, @group)
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'new_epic'
#new-epic-app{ data: { endpoint: request.url } }
= link_to group_epics_path(@group), title: 'List', class: 'btn' do
%span= _('View epics list')
......@@ -113,6 +113,18 @@ describe Groups::EpicsController do
expect(item['end_date']).to eq(epic.end_date)
expect(item['web_url']).to eq(group_epic_path(group, epic))
end
context 'using label_name filter' do
let(:label) { create(:label) }
let!(:labeled_epic) { create(:labeled_epic, group: group, labels: [label]) }
it 'returns all epics with given label' do
get :index, group_id: group, label_name: label.title, format: :json
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(labeled_epic.id)
end
end
end
end
......
......@@ -8,13 +8,14 @@ module Gitlab
private
def base_query
@base_query ||= stage_query
@base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def stage_query
def stage_query(project_ids)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests
......
......@@ -21,17 +21,28 @@ module Gitlab
end
def median
BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
cte_table = Arel::Table.new("cte_table_for_#{name}")
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current
# cycle analytics stage.
interval_query = Arel::Nodes::As.new(
cte_table,
subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
interval_query = Arel::Nodes::As.new(cte_table,
subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
median_datetime(cte_table, interval_query, name)
if project_ids.one?
loader.call(@project.id, median_datetime(cte_table, interval_query, name))
else
begin
median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
end
rescue NotSupportedError
{}
end
end
end
end
def name
......
module Gitlab
module CycleAnalytics
module ProductionHelper
def stage_query
super
def stage_query(project_ids)
super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
......
......@@ -25,11 +25,11 @@ module Gitlab
_("Total test time for all commits/merges")
end
def stage_query
def stage_query(project_ids)
if @options[:branch]
super.where(build_table[:ref].eq(@options[:branch]))
super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else
super
super(project_ids)
end
end
end
......
module Gitlab
module CycleAnalytics
class UsageData
PROJECTS_LIMIT = 10
attr_reader :projects, :options
def initialize
@projects = Project.sorted_by_activity.limit(PROJECTS_LIMIT)
@options = { from: 7.days.ago }
end
def to_json
total = 0
values =
medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
calculations = stage_values(medians)
total += calculations.values.compact.sum
hsh[stage_name] = calculations
end
values[:total] = total
{ avg_cycle_analytics: values }
end
private
def medians_per_stage
projects.each_with_object({}) do |project, hsh|
::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end
end
end
def stage_values(medians)
medians = medians.map(&:presence).compact
average = calc_average(medians)
{
average: average,
sd: standard_deviation(medians, average),
missing: projects.length - medians.length
}
end
def calc_average(values)
return if values.empty?
(values.sum / values.length).to_i
end
def standard_deviation(values, average)
Math.sqrt(sample_variance(values, average)).to_i
end
def sample_variance(values, average)
return 0 if values.length <= 1
sum = values.inject(0) do |acc, val|
acc + (val - average)**2
end
sum / (values.length - 1)
end
end
end
end
......@@ -2,18 +2,14 @@
module Gitlab
module Database
module Median
NotSupportedError = Class.new(StandardError)
def median_datetime(arel_table, query_so_far, column_sym)
median_queries =
if Gitlab::Database.postgresql?
pg_median_datetime_sql(arel_table, query_so_far, column_sym)
elsif Gitlab::Database.mysql?
mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
extract_median(execute_queries(arel_table, query_so_far, column_sym)).presence
end
results = Array.wrap(median_queries).map do |query|
ActiveRecord::Base.connection.execute(query)
end
extract_median(results).presence
def median_datetimes(arel_table, query_so_far, column_sym, partition_column)
extract_medians(execute_queries(arel_table, query_so_far, column_sym, partition_column)).presence
end
def extract_median(results)
......@@ -21,13 +17,21 @@ module Gitlab
if Gitlab::Database.postgresql?
result = result.first.presence
median = result['median'] if result
median.to_f if median
result['median']&.to_f if result
elsif Gitlab::Database.mysql?
result.to_a.flatten.first
end
end
def extract_medians(results)
median_values = results.compact.first.values
median_values.each_with_object({}) do |(id, median), hash|
hash[id.to_i] = median&.to_f
end
end
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
query = arel_table
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
......@@ -53,7 +57,7 @@ module Gitlab
]
end
def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
def pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column = nil)
# Create a CTE with the column we're operating on, row number (after sorting by the column
# we're operating on), and count of the table we're operating on (duplicated across) all rows
# of the CTE. For example, if we're looking to find the median of the `projects.star_count`
......@@ -64,22 +68,31 @@ module Gitlab
# 5 | 1 | 3
# 9 | 2 | 3
# 15 | 3 | 3
#
# If a partition column is used we will do the same operation but for separate partitions,
# when that happens the CTE might look like this:
#
# project_id | star_count | row_id | ct
# ------------+------------+--------+----
# 1 | 5 | 1 | 2
# 1 | 9 | 2 | 2
# 2 | 10 | 1 | 3
# 2 | 15 | 2 | 3
# 2 | 20 | 3 | 3
cte_table = Arel::Table.new("ordered_records")
cte = Arel::Nodes::As.new(
cte_table,
arel_table
.project(
arel_table[column_sym].as(column_sym.to_s),
Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
arel_table.project("COUNT(1)").as('ct')).
arel_table.project(*rank_rows(arel_table, column_sym, partition_column)).
# Disallow negative values
where(arel_table[column_sym].gteq(zero_interval)))
# From the CTE, select either the middle row or the middle two rows (this is accomplished
# by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
# selected rows, and this is the median value.
cte_table.project(average([extract_epoch(cte_table[column_sym])], "median"))
result =
cte_table
.project(*median_projections(cte_table, column_sym, partition_column))
.where(
Arel::Nodes::Between.new(
cte_table[:row_id],
......@@ -90,15 +103,72 @@ module Gitlab
)
)
.with(query_so_far, cte)
.to_sql
result.group(cte_table[partition_column]).order(cte_table[partition_column]) if partition_column
result.to_sql
end
private
def median_queries(arel_table, query_so_far, column_sym, partition_column = nil)
if Gitlab::Database.postgresql?
pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
elsif Gitlab::Database.mysql?
raise NotSupportedError, "partition_column is not supported for MySQL" if partition_column
mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
end
end
def execute_queries(arel_table, query_so_far, column_sym, partition_column = nil)
queries = median_queries(arel_table, query_so_far, column_sym, partition_column)
Array.wrap(queries).map { |query| ActiveRecord::Base.connection.execute(query) }
end
def average(args, as)
Arel::Nodes::NamedFunction.new("AVG", args, as)
end
def rank_rows(arel_table, column_sym, partition_column)
column_row = arel_table[column_sym].as(column_sym.to_s)
if partition_column
partition_row = arel_table[partition_column]
row_id =
Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('rank', []),
Arel::Nodes::Window.new.partition(arel_table[partition_column])
.order(arel_table[column_sym])
).as('row_id')
count = arel_table.from(arel_table.alias)
.project('COUNT(*)')
.where(arel_table[partition_column].eq(arel_table.alias[partition_column]))
.as('ct')
[partition_row, column_row, row_id, count]
else
row_id =
Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('row_number', []),
Arel::Nodes::Window.new.order(arel_table[column_sym])
).as('row_id')
count = arel_table.project("COUNT(1)").as('ct')
[column_row, row_id, count]
end
end
def median_projections(table, column_sym, partition_column)
projections = []
projections << table[partition_column] if partition_column
projections << average([extract_epoch(table[column_sym])], "median")
projections
end
def extract_epoch(arel_attribute)
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end
......
......@@ -1042,6 +1042,21 @@ module Gitlab
end
end
def license_short_name
gitaly_migrate(:license_short_name) do |is_enabled|
if is_enabled
gitaly_repository_client.license_short_name
else
begin
# The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
Licensee.license(path).try(:key)
rescue Rugged::Error
end
end
end
end
def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository)
start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
......
......@@ -249,6 +249,14 @@ module Gitlab
raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
end
def license_short_name
request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
response.license_short_name.presence
end
end
end
end
......@@ -19,6 +19,7 @@ module Gitlab
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.test_env = Rails.env.test?
if current_user
gon.current_user_id = current_user.id
......
......@@ -9,6 +9,7 @@ module Gitlab
license_usage_data.merge(system_usage_data)
.merge(features_usage_data)
.merge(components_usage_data)
.merge(cycle_analytics_usage_data)
end
def to_json(force_refresh: false)
......@@ -108,6 +109,10 @@ module Gitlab
}
end
def cycle_analytics_usage_data
Gitlab::CycleAnalytics::UsageData.new.to_json
end
def features_usage_data
features_usage_data_ce.merge(features_usage_data_ee)
end
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-23 13:23+0100\n"
"PO-Revision-Date: 2018-02-23 13:23+0100\n"
"POT-Creation-Date: 2018-03-01 22:35+0100\n"
"PO-Revision-Date: 2018-03-01 22:35+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -51,6 +51,9 @@ msgid_plural "%s additional commits have been omitted to prevent performance iss
msgstr[0] ""
msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
......@@ -71,6 +74,9 @@ msgstr ""
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
......@@ -758,6 +764,9 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
msgid "Click to expand text"
msgstr ""
......@@ -911,9 +920,6 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
msgid "ClusterIntegration|Learn more about Kubernetes"
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
msgstr ""
......@@ -1049,6 +1055,12 @@ msgstr ""
msgid "Collapse"
msgstr ""
msgid "Comment and resolve discussion"
msgstr ""
msgid "Comment and unresolve discussion"
msgstr ""
msgid "Comments"
msgstr ""
......@@ -1074,6 +1086,9 @@ msgstr ""
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
msgid "Commit to %{branchName} branch"
msgstr ""
msgid "CommitBoxTitle|Commit"
msgstr ""
......@@ -1230,6 +1245,12 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
msgid "Create a new branch"
msgstr ""
msgid "Create a new branch and merge request"
msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
......@@ -1281,6 +1302,12 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
msgid "Creates a new branch from %{branchName}"
msgstr ""
msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
msgstr ""
msgid "Creating epic"
msgstr ""
......@@ -1370,6 +1397,9 @@ msgstr ""
msgid "Disable"
msgstr ""
msgid "Discard draft"
msgstr ""
msgid "Discover GitLab Geo."
msgstr ""
......@@ -1490,6 +1520,12 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
msgid "Error checking branch data. Please try again."
msgstr ""
msgid "Error committing changes. Please try again."
msgstr ""
msgid "Error creating epic"
msgstr ""
......@@ -1639,6 +1675,9 @@ msgstr ""
msgid "From merge request merge until deploy to production"
msgstr ""
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
msgid "GPG Keys"
msgstr ""
......@@ -1917,6 +1956,9 @@ msgstr ""
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr ""
msgid "Install Runner on Kubernetes"
msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
......@@ -2050,6 +2092,9 @@ msgstr ""
msgid "Learn more"
msgstr ""
msgid "Learn more about Kubernetes"
msgstr ""
msgid "Learn more about protected branches"
msgstr ""
......@@ -2128,9 +2173,6 @@ msgstr ""
msgid "Members"
msgstr ""
msgid "Merge Request"
msgstr ""
msgid "Merge Requests"
msgstr ""
......@@ -2877,6 +2919,9 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
msgid "Resolve discussion"
msgstr ""
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
......@@ -2945,6 +2990,9 @@ msgstr ""
msgid "Select a timezone"
msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
msgid "Select assignee"
msgstr ""
......@@ -2987,6 +3035,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
msgstr ""
......@@ -3040,7 +3091,7 @@ msgstr ""
msgid "Something went wrong when toggling the button"
msgstr ""
msgid "Something went wrong while closing the issue. Please try again later"
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching SAST."
......@@ -3052,7 +3103,10 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
msgid "Something went wrong while reopening the issue. Please try again later"
msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
msgid "Something went wrong. Please try again."
......@@ -3670,6 +3724,9 @@ msgstr ""
msgid "Unlocked"
msgstr ""
msgid "Unresolve discussion"
msgstr ""
msgid "Unstar"
msgstr ""
......@@ -3754,6 +3811,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
msgid "Web IDE"
msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
......@@ -3874,6 +3934,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
msgid "Write a commit message..."
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
......@@ -3892,6 +3955,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
msgid "You can move around the graph by using the arrow keys."
msgstr ""
......@@ -3958,6 +4024,9 @@ msgstr ""
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr ""
msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
msgstr ""
msgid "Your comment will not be visible to the public."
msgstr ""
......@@ -4073,6 +4142,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
msgid "mrWidget|Add approval"
msgstr ""
msgid "mrWidget|An error occured while removing your approval."
msgstr ""
......@@ -4085,9 +4157,6 @@ msgstr ""
msgid "mrWidget|Approve"
msgstr ""
msgid "mrWidget|Add approval"
msgstr ""
msgid "mrWidget|Approved by"
msgstr ""
......@@ -4266,3 +4335,6 @@ msgstr ""
msgid "uses Kubernetes clusters to deploy your code!"
msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
require 'spec_helper'
describe Groups::LabelsController do
let(:group) { create(:group) }
let(:user) { create(:user) }
set(:group) { create(:group) }
set(:user) { create(:user) }
set(:project) { create(:project, namespace: group) }
before do
group.add_owner(user)
......@@ -10,6 +11,34 @@ describe Groups::LabelsController do
sign_in(user)
end
describe 'GET #index' do
set(:label_1) { create(:label, project: project, title: 'label_1') }
set(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') }
it 'returns group and project labels by default' do
get :index, group_id: group, format: :json
label_ids = json_response.map {|label| label['title']}
expect(label_ids).to match_array([label_1.title, group_label_1.title])
end
context 'with ancestor group', :nested_groups do
set(:subgroup) { create(:group, parent: group) }
set(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') }
before do
subgroup.add_owner(user)
end
it 'returns ancestor group labels', :nested_groups do
get :index, group_id: subgroup, include_ancestor_groups: true, only_group_labels: true, format: :json
label_ids = json_response.map {|label| label['title']}
expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
end
end
end
describe 'POST #toggle_subscription' do
it 'allows user to toggle subscription on group labels' do
label = create(:group_label, group: group)
......
......@@ -91,6 +91,12 @@ describe Projects::ClustersController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('cluster_status')
end
it 'invokes schedule_status_update on each application' do
expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
go
end
end
describe 'security' do
......
......@@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do
milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone)
create_merge_request_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue)
end
it 'is false' do
......
......@@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
context 'as an allowed user' do
......@@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
project.add_master(user)
create_cycle
deploy_master
@build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
sign_in(user)
visit project_cycle_analytics_path(project)
......@@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do
project.add_guest(guest)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle
deploy_master
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
sign_in(guest)
visit project_cycle_analytics_path(project)
......@@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do
expect(find('.stage-events')).to have_content("!#{mr.iid}")
end
def create_cycle
issue.update(milestone: milestone)
pipeline.run
@build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
def click_stage(stage_name)
find('.stage-nav li', text: stage_name).click
wait_for_requests
......
......@@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do
scenario 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end
end
end
......@@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do
scenario 'user can install applications' do
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end
end
context 'when user installs Helm' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
allow(ClusterInstallAppWorker).to receive(:perform_async)
page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click
......@@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do
page.within('.js-cluster-application-row-helm') do
# FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
Clusters::Cluster.last.application_helm.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
end
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
......@@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do
context 'when user installs Ingress' do
context 'when user installs application: Ingress' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
allow(ClusterInstallAppWorker).to receive(:perform_async)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
create(:clusters_applications_helm, :installed, cluster: cluster)
page.within('.js-cluster-application-row-ingress') do
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
page.find(:css, '.js-cluster-application-install-button').click
end
end
......@@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do
it 'he sees status transition' do
page.within('.js-cluster-application-row-ingress') do
# FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_ingress.make_installing!
# FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
expect(page).to have_css('.js-cluster-application-install-button[disabled]')
# The application becomes installed but we keep waiting for external IP address
Clusters::Cluster.last.application_ingress.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page).to have_selector('.js-no-ip-message')
expect(page.find('.js-ip-address').value).to eq('?')
# We receive the external IP address and display
Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
expect(page).not_to have_selector('.js-no-ip-message')
expect(page.find('.js-ip-address').value).to eq('192.168.1.100')
end
expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
......
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
before do
allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true)
end
def manage_two_factor_authentication
click_on 'Manage two-factor authentication'
expect(page).to have_content("Setup new U2F device")
......
......@@ -89,6 +89,25 @@ describe LabelsFinder do
expect(finder.execute).to eq [private_subgroup_label_1]
end
end
context 'when including labels from group descendants', :nested_groups do
it 'returns labels from group and its descendants' do
private_group_1.add_developer(user)
private_subgroup_1.add_developer(user)
finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true)
expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1]
end
it 'ignores labels from groups which user can not read' do
private_subgroup_1.add_developer(user)
finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true)
expect(finder.execute).to eq [private_subgroup_label_1]
end
end
end
context 'filtering by project_id' do
......
......@@ -30,7 +30,8 @@
]
}
},
"status_reason": { "type": ["string", "null"] }
"status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
......
require 'spec_helper'
describe U2fHelper do
describe 'when not on mobile' do
it 'does not inject u2f on chrome 40' do
device = double(mobile?: false)
browser = double(chrome?: true, opera?: false, version: 40, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
it 'injects u2f on chrome 41' do
device = double(mobile?: false)
browser = double(chrome?: true, opera?: false, version: 41, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq true
end
it 'does not inject u2f on opera 39' do
device = double(mobile?: false)
browser = double(chrome?: false, opera?: true, version: 39, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
it 'injects u2f on opera 40' do
device = double(mobile?: false)
browser = double(chrome?: false, opera?: true, version: 40, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq true
end
end
describe 'when on mobile' do
it 'does not inject u2f on chrome 41' do
device = double(mobile?: true)
browser = double(chrome?: true, opera?: false, version: 41, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
it 'does not inject u2f on opera 40' do
device = double(mobile?: true)
browser = double(chrome?: false, opera?: true, version: 40, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
end
end
......@@ -44,4 +44,71 @@ describe('Applications', () => {
});
/* */
});
describe('Ingress application', () => {
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '0.0.0.0',
},
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
).toEqual('0.0.0.0');
});
});
describe('without ip address', () => {
it('renders an input text with a question mark and an alert text', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
},
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
});
});
describe('before installing', () => {
it('does not render the IP address', () => {
vm = mountComponent(Applications, {
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(vm.$el.textContent).not.toContain('Ingress IP Address');
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
});
});
......@@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'ingress',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
external_ip: null,
}, {
name: 'runner',
status: APPLICATION_INSTALLING,
......
......@@ -75,6 +75,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[1].status_reason,
requestStatus: null,
requestReason: null,
externalIp: null,
},
runner: {
title: 'GitLab Runner',
......
import Vue from 'vue';
import eventHub from 'ee/issuable/related_issues/event_hub';
import addIssuableForm from 'ee/issuable/related_issues/components/add_issuable_form.vue';
import eventHub from 'ee/related_issues/event_hub';
import addIssuableForm from 'ee/related_issues/components/add_issuable_form.vue';
const issuable1 = {
id: 200,
......
import Vue from 'vue';
import issueItem from 'ee/issuable/related_issues/components/issue_item.vue';
import eventHub from 'ee/issuable/related_issues/event_hub';
import issueItem from 'ee/related_issues/components/issue_item.vue';
import eventHub from 'ee/related_issues/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('issueItem', () => {
......
import Vue from 'vue';
import eventHub from 'ee/issuable/related_issues/event_hub';
import issueToken from 'ee/issuable/related_issues/components/issue_token.vue';
import eventHub from 'ee/related_issues/event_hub';
import issueToken from 'ee/related_issues/components/issue_token.vue';
describe('IssueToken', () => {
const idKey = 200;
......
import Vue from 'vue';
import eventHub from 'ee/issuable/related_issues/event_hub';
import relatedIssuesBlock from 'ee/issuable/related_issues/components/related_issues_block.vue';
import eventHub from 'ee/related_issues/event_hub';
import relatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import { issuable1, issuable2, issuable3, issuable4, issuable5 } from '../mock_data';
......
import Vue from 'vue';
import _ from 'underscore';
import relatedIssuesRoot from 'ee/issuable/related_issues/components/related_issues_root.vue';
import relatedIssuesService from 'ee/issuable/related_issues/services/related_issues_service';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import relatedIssuesService from 'ee/related_issues/services/related_issues_service';
import { defaultProps, issuable1, issuable2 } from '../mock_data';
......
import RelatedIssuesStore from 'ee/issuable/related_issues/stores/related_issues_store';
import RelatedIssuesStore from 'ee/related_issues/stores/related_issues_store';
import { issuable1, issuable2, issuable3, issuable4, issuable5 } from '../mock_data';
......
......@@ -36,6 +36,7 @@ window.$ = window.jQuery = $;
window.gl = window.gl || {};
window.gl.TEST_HOST = 'http://test.host';
window.gon = window.gon || {};
window.gon.test_env = true;
let hasUnhandledPromiseRejections = false;
......
......@@ -5,7 +5,7 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FAuthenticate', () => {
preloadFixtures('u2f/authenticate.html.raw');
beforeEach(() => {
beforeEach((done) => {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-authenticate-u2f');
......@@ -22,7 +22,7 @@ describe('U2FAuthenticate', () => {
// bypass automatic form submission within renderAuthenticated
spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
return this.component.start();
this.component.start().then(done).catch(done.fail);
});
it('allows authenticating via a U2F device', () => {
......@@ -34,7 +34,7 @@ describe('U2FAuthenticate', () => {
expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
return describe('errors', () => {
describe('errors', () => {
it('displays an error message', () => {
const setupButton = this.container.find('#js-login-u2f-device');
setupButton.trigger('click');
......
......@@ -5,12 +5,12 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FRegister', () => {
preloadFixtures('u2f/register.html.raw');
beforeEach(() => {
beforeEach((done) => {
loadFixtures('u2f/register.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-register-u2f');
this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token');
return this.component.start();
this.component.start().then(done).catch(done.fail);
});
it('allows registering a U2F device', () => {
......
import { canInjectU2fApi } from '~/u2f/util';
describe('U2F Utils', () => {
describe('canInjectU2fApi', () => {
it('returns false for Chrome < 41', () => {
const userAgent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36';
expect(canInjectU2fApi(userAgent)).toBe(false);
});
it('returns true for Chrome >= 41', () => {
const userAgent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36';
expect(canInjectU2fApi(userAgent)).toBe(true);
});
it('returns false for Opera < 40', () => {
const userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25';
expect(canInjectU2fApi(userAgent)).toBe(false);
});
it('returns true for Opera >= 40', () => {
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991';
expect(canInjectU2fApi(userAgent)).toBe(true);
});
it('returns false for Safari', () => {
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4';
expect(canInjectU2fApi(userAgent)).toBe(false);
});
it('returns false for Chrome on Android', () => {
const userAgent = 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36';
expect(canInjectU2fApi(userAgent)).toBe(false);
});
it('returns false for Chrome on iOS', () => {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1';
expect(canInjectU2fApi(userAgent)).toBe(false);
});
it('returns false for Safari on iOS', () => {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1';
expect(canInjectU2fApi(userAgent)).toBe(false);
});
});
});
......@@ -26,7 +26,6 @@ describe Gitlab::Auth::LDAP::User do
let(:auth_hash_upper_case) do
OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
end
let!(:fake_proxy) { fake_ldap_sync_proxy('ldapmain') }
describe '#changed?' do
it "marks existing ldap user as changed" do
......
......@@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
create_merge_request_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue)
end
end
end
......@@ -236,8 +236,8 @@ describe 'cycle analytics events' do
pipeline.run!
pipeline.succeed!
merge_merge_requests_closing_issue(context)
deploy_master
merge_merge_requests_closing_issue(user, project, context)
deploy_master(user, project)
end
it 'has the name' do
......@@ -294,8 +294,8 @@ describe 'cycle analytics events' do
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
merge_merge_requests_closing_issue(context)
deploy_master
merge_merge_requests_closing_issue(user, project, context)
deploy_master(user, project)
end
it 'has the total time' do
......@@ -334,7 +334,7 @@ describe 'cycle analytics events' do
def setup(context)
milestone = create(:milestone, project: project)
context.update(milestone: milestone)
mr = create_merge_request_closing_issue(context, commit_message: "References #{context.to_reference}")
mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}")
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
......
require 'spec_helper'
describe Gitlab::CycleAnalytics::UsageData do
describe '#to_json' do
before do
Timecop.freeze do
user = create(:user, :admin)
projects = create_list(:project, 2, :repository)
projects.each_with_index do |project, time|
issue = create(:issue, project: project, created_at: (time + 1).hour.ago)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
milestone = create(:milestone, project: project)
mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project, environment: 'staging')
deploy_master(user, project)
end
end
end
shared_examples 'a valid usage data result' do
it 'returns the aggregated usage data of every selected project' do
result = subject.to_json
expect(result).to have_key(:avg_cycle_analytics)
CycleAnalytics::STAGES.each do |stage|
expect(result[:avg_cycle_analytics]).to have_key(stage)
stage_values = result[:avg_cycle_analytics][stage]
expected_values = expect_values_per_stage[stage]
expected_values.each_pair do |op, value|
expect(stage_values).to have_key(op)
if op == :missing
expect(stage_values[op]).to eq(value)
else
# delta is used because of git timings that Timecop does not stub
expect(stage_values[op].to_i).to be_within(5).of(value.to_i)
end
end
end
end
end
context 'when using postgresql', :postgresql do
let(:expect_values_per_stage) do
{
issue: {
average: 5400,
sd: 2545,
missing: 0
},
plan: {
average: 2,
sd: 2,
missing: 0
},
code: {
average: nil,
sd: 0,
missing: 2
},
test: {
average: nil,
sd: 0,
missing: 2
},
review: {
average: 0,
sd: 0,
missing: 0
},
staging: {
average: 0,
sd: 0,
missing: 0
},
production: {
average: 5400,
sd: 2545,
missing: 0
}
}
end
it_behaves_like 'a valid usage data result'
end
context 'when using mysql', :mysql do
let(:expect_values_per_stage) do
{
issue: {
average: nil,
sd: 0,
missing: 2
},
plan: {
average: nil,
sd: 0,
missing: 2
},
code: {
average: nil,
sd: 0,
missing: 2
},
test: {
average: nil,
sd: 0,
missing: 2
},
review: {
average: nil,
sd: 0,
missing: 2
},
staging: {
average: nil,
sd: 0,
missing: 2
},
production: {
average: nil,
sd: 0,
missing: 2
}
}
end
it_behaves_like 'a valid usage data result'
end
end
end
require 'spec_helper'
describe Gitlab::Database::Median do
let(:dummy_class) do
Class.new do
include Gitlab::Database::Median
end
end
subject(:median) { dummy_class.new }
describe '#median_datetimes' do
it 'raises NotSupportedError', :mysql do
expect { median.median_datetimes(nil, nil, nil, :project_id) }.to raise_error(dummy_class::NotSupportedError, "partition_column is not supported for MySQL")
end
end
end
......@@ -1689,6 +1689,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#license_short_name' do
shared_examples 'acquiring the Licensee license key' do
subject { repository.license_short_name }
context 'when no license file can be found' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
it { is_expected.to be_nil }
end
context 'when an mit license is found' do
it { is_expected.to eq('mit') }
end
end
context 'when gitaly is enabled' do
it_behaves_like 'acquiring the Licensee license key'
end
context 'when gitaly is disabled', :disable_gitaly do
it_behaves_like 'acquiring the Licensee license key'
end
end
describe '#with_repo_branch_commit' do
context 'when comparing with the same repository' do
let(:start_repository) { repository }
......
......@@ -49,6 +49,7 @@ describe Gitlab::UsageData do
geo
git
database
avg_cycle_analytics
))
end
......
......@@ -4,5 +4,52 @@ describe Clusters::Applications::Ingress do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
include_examples 'cluster application specs', described_class
describe '#make_installed!' do
before do
application.make_installed!
end
let(:application) { create(:clusters_applications_ingress, :installing) }
it 'schedules a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in)
.with(Clusters::Applications::Ingress::FETCH_IP_ADDRESS_DELAY, 'ingress', application.id)
end
end
describe '#schedule_status_update' do
let(:application) { create(:clusters_applications_ingress, :installed) }
before do
application.schedule_status_update
end
it 'schedules a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async)
.with('ingress', application.id)
end
context 'when the application is not installed' do
let(:application) { create(:clusters_applications_ingress, :installing) }
it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async)
end
end
context 'when there is already an external_ip' do
let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '111.222.222.111') }
it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
end
end
end
end
......@@ -18,11 +18,11 @@ describe 'CycleAnalytics#code' do
end]],
end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
context.create_merge_request_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.deploy_master
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
context.deploy_master(context.user, context.project)
end)
context "when a regular merge request (that doesn't close the issue) is created" do
......@@ -30,10 +30,10 @@ describe 'CycleAnalytics#code' do
issue = create(:issue, project: project)
create_commit_referencing_issue(issue)
create_merge_request_closing_issue(issue, message: "Closes nothing")
create_merge_request_closing_issue(user, project, issue, message: "Closes nothing")
merge_merge_requests_closing_issue(issue)
deploy_master
merge_merge_requests_closing_issue(user, project, issue)
deploy_master(user, project)
expect(subject[:code].median).to be_nil
end
......@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code' do
end]],
end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
context.create_merge_request_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when a regular merge request (that doesn't close the issue) is created" do
......@@ -61,9 +61,9 @@ describe 'CycleAnalytics#code' do
issue = create(:issue, project: project)
create_commit_referencing_issue(issue)
create_merge_request_closing_issue(issue, message: "Closes nothing")
create_merge_request_closing_issue(user, project, issue, message: "Closes nothing")
merge_merge_requests_closing_issue(issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:code].median).to be_nil
end
......
......@@ -26,8 +26,8 @@ describe 'CycleAnalytics#issue' do
end]],
post_fn: -> (context, data) do
if data[:issue].persisted?
context.create_merge_request_closing_issue(data[:issue].reload)
context.merge_merge_requests_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue].reload)
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end
end)
......@@ -37,8 +37,8 @@ describe 'CycleAnalytics#issue' do
issue = create(:issue, project: project)
issue.update(label_ids: [regular_label.id])
create_merge_request_closing_issue(issue)
merge_merge_requests_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil
end
......
......@@ -29,8 +29,8 @@ describe 'CycleAnalytics#plan' do
context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
end]],
post_fn: -> (context, data) do
context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
context.merge_merge_requests_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue], source_branch: data[:branch_name])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when a regular label (instead of a list label) is added to the issue" do
......@@ -41,8 +41,8 @@ describe 'CycleAnalytics#plan' do
issue.update(label_ids: [label.id])
create_commit_referencing_issue(issue, branch_name: branch_name)
create_merge_request_closing_issue(issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil
end
......
......@@ -13,11 +13,11 @@ describe 'CycleAnalytics#production' do
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
before_end_fn: lambda do |context, data|
context.create_merge_request_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end,
end_time_conditions:
[["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
[["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
......@@ -29,14 +29,14 @@ describe 'CycleAnalytics#production' do
branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master
deploy_master(user, project)
expect(subject[:production].median).to be_nil
end
......@@ -45,9 +45,9 @@ describe 'CycleAnalytics#production' do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(environment: 'staging')
deploy_master(user, project, environment: 'staging')
expect(subject[:production].median).to be_nil
end
......
......@@ -13,11 +13,11 @@ describe 'CycleAnalytics#review' do
data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
start_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
context.create_merge_request_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
end_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: nil)
......
......@@ -13,15 +13,15 @@ describe 'CycleAnalytics#staging' do
phase: :staging,
data_fn: lambda do |context|
issue = context.create(:issue, project: context.project)
{ issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
{ issue: issue, merge_request: context.create_merge_request_closing_issue(context.user, context.project, issue) }
end,
start_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end]],
end_time_conditions: [["merge request that closes issue is deployed to production",
-> (context, data) do
context.deploy_master
context.deploy_master(context.user, context.project)
end],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
......@@ -34,14 +34,14 @@ describe 'CycleAnalytics#staging' do
branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master
deploy_master(user, project)
expect(subject[:staging].median).to be_nil
end
......@@ -50,9 +50,9 @@ describe 'CycleAnalytics#staging' do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(environment: 'staging')
deploy_master(user, project, environment: 'staging')
expect(subject[:staging].median).to be_nil
end
......
......@@ -12,26 +12,26 @@ describe 'CycleAnalytics#test' do
phase: :test,
data_fn: lambda do |context|
issue = context.create(:issue, project: context.project)
merge_request = context.create_merge_request_closing_issue(issue)
merge_request = context.create_merge_request_closing_issue(context.user, context.project, issue)
pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request)
{ pipeline: pipeline, issue: issue }
end,
start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run!
pipeline.succeed!
merge_merge_requests_closing_issue(issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil
end
......@@ -51,13 +51,13 @@ describe 'CycleAnalytics#test' do
context "when the pipeline is dropped (failed)" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run!
pipeline.drop!
merge_merge_requests_closing_issue(issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil
end
......@@ -66,13 +66,13 @@ describe 'CycleAnalytics#test' do
context "when the pipeline is cancelled" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run!
pipeline.cancel!
merge_merge_requests_closing_issue(issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil
end
......
require 'spec_helper'
describe CycleAnalytics do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
subject { described_class.new(project, from: from_date) }
describe '#all_medians_per_stage' do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
end
it 'returns every median for each stage for a specific project' do
values = described_class::STAGES.each_with_object({}) do |stage_name, hsh|
hsh[stage_name] = subject[stage_name].median.presence
end
expect(subject.all_medians_per_stage).to eq(values)
end
end
end
......@@ -15,7 +15,7 @@ describe 'cycle analytics events' do
end
end
deploy_master
deploy_master(user, project)
login_as(user)
end
......@@ -119,7 +119,7 @@ describe 'cycle analytics events' do
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
mr = create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}")
mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
pipeline.run
......@@ -127,7 +127,7 @@ describe 'cycle analytics events' do
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(issue)
merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
......
......@@ -26,5 +26,19 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to eq(application.status_reason)
end
end
context 'for ingress application' do
let(:application) do
build(
:clusters_applications_ingress,
:installed,
external_ip: '111.222.111.222'
)
end
it 'includes external_ip' do
expect(subject[:external_ip]).to eq('111.222.111.222')
end
end
end
end
require 'spec_helper'
describe Clusters::Applications::CheckIngressIpAddressService do
let(:application) { create(:clusters_applications_ingress, :installed) }
let(:service) { described_class.new(application) }
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
let(:ingress) { [{ ip: '111.222.111.222' }] }
let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) }
let(:kube_service) do
::Kubeclient::Resource.new(
{
status: {
loadBalancer: {
ingress: ingress
}
}
}
)
end
subject { service.execute }
before do
allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
allow(Gitlab::ExclusiveLease)
.to receive(:new)
.with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i)
.and_return(exclusive_lease)
end
describe '#execute' do
context 'when the ingress ip address is available' do
it 'updates the external_ip for the app' do
subject
expect(application.external_ip).to eq('111.222.111.222')
end
end
context 'when the ingress ip address is not available' do
let(:ingress) { nil }
it 'does not error' do
subject
end
end
context 'when the exclusive lease cannot be obtained' do
before do
allow(exclusive_lease)
.to receive(:try_obtain)
.and_return(false)
end
it 'does not call kubeclient' do
subject
expect(kubeclient).not_to have_received(:get_service)
end
end
context 'when there is already an external_ip' do
let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '001.111.002.111') }
it 'does not call kubeclient' do
subject
expect(kubeclient).not_to have_received(:get_service)
end
end
end
end
......@@ -204,6 +204,10 @@ RSpec.configure do |config|
example.run if Gitlab::Database.postgresql?
end
config.around(:each, :mysql) do |example|
example.run if Gitlab::Database.mysql?
end
# This makes sure the `ApplicationController#can?` method is stubbed with the
# original implementation for all view specs.
config.before(:each, type: :view) do
......
......@@ -26,7 +26,19 @@ module CycleAnalyticsHelpers
ref: 'refs/heads/master').execute
end
def create_merge_request_closing_issue(issue, message: nil, source_branch: nil, commit_message: 'commit message')
def create_cycle(user, project, issue, mr, milestone, pipeline)
issue.update(milestone: milestone)
pipeline.run
ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
ci_build
end
def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message')
if !source_branch || project.repository.commit(source_branch).blank?
source_branch = generate(:branch)
project.repository.add_branch(user, source_branch, 'master')
......@@ -52,19 +64,19 @@ module CycleAnalyticsHelpers
mr
end
def merge_merge_requests_closing_issue(issue)
def merge_merge_requests_closing_issue(user, project, issue)
merge_requests = issue.closed_by_merge_requests(user)
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end
def deploy_master(environment: 'production')
def deploy_master(user, project, environment: 'production')
dummy_job =
case environment
when 'production'
dummy_production_job
dummy_production_job(user, project)
when 'staging'
dummy_staging_job
dummy_staging_job(user, project)
else
raise ArgumentError
end
......@@ -72,16 +84,15 @@ module CycleAnalyticsHelpers
CreateDeploymentService.new(dummy_job).execute
end
def dummy_production_job
@dummy_job ||= new_dummy_job('production')
def dummy_production_job(user, project)
new_dummy_job(user, project, 'production')
end
def dummy_staging_job
@dummy_job ||= new_dummy_job('staging')
def dummy_staging_job(user, project)
new_dummy_job(user, project, 'staging')
end
def dummy_pipeline
@dummy_pipeline ||=
def dummy_pipeline(project)
Ci::Pipeline.new(
sha: project.repository.commit('master').sha,
ref: 'master',
......@@ -90,7 +101,7 @@ module CycleAnalyticsHelpers
protected: false)
end
def new_dummy_job(environment)
def new_dummy_job(user, project, environment)
project.environments.find_or_create_by(name: environment)
Ci::Build.new(
......@@ -101,7 +112,7 @@ module CycleAnalyticsHelpers
tag: false,
name: 'dummy',
stage: 'dummy',
pipeline: dummy_pipeline,
pipeline: dummy_pipeline(project),
protected: false)
end
......
require 'spec_helper'
describe ClusterWaitForIngressIpAddressWorker do
describe '#perform' do
let(:service) { instance_double(Clusters::Applications::CheckIngressIpAddressService, execute: true) }
let(:application) { instance_double(Clusters::Applications::Ingress) }
let(:worker) { described_class.new }
before do
allow(worker)
.to receive(:find_application)
.with('ingress', 117)
.and_yield(application)
allow(Clusters::Applications::CheckIngressIpAddressService)
.to receive(:new)
.with(application)
.and_return(service)
allow(described_class)
.to receive(:perform_in)
end
it 'finds the application and calls CheckIngressIpAddressService#execute' do
worker.perform('ingress', 117)
expect(service).to have_received(:execute)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment