Commit c8f3a60d authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-jobs-list-components

parents aab71313 4c74936f
...@@ -181,7 +181,7 @@ Team labels specify what team is responsible for this issue. ...@@ -181,7 +181,7 @@ Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate Assigning a team label makes sure issues get the attention of the appropriate
people. people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality, The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX". ~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the The descriptions on the [labels page][labels-page] explain what falls under the
......
...@@ -162,7 +162,7 @@ gem 'acts-as-taggable-on', '~> 5.0' ...@@ -162,7 +162,7 @@ gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 5.1' gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0' gem 'sidekiq-cron', '~> 0.6.0'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.6.0'
gem 'sidekiq-limit_fetch', '~> 3.4', require: false gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser # Cron Parser
...@@ -412,7 +412,7 @@ group :ed25519 do ...@@ -412,7 +412,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -281,7 +281,7 @@ GEM ...@@ -281,7 +281,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.99.0) gitaly-proto (0.100.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -709,8 +709,8 @@ GEM ...@@ -709,8 +709,8 @@ GEM
redis-activesupport (5.0.4) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.6.0)
redis (~> 3.0, >= 3.0.4) redis (>= 3.0.4)
redis-rack (2.0.4) redis-rack (2.0.4)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
...@@ -918,7 +918,7 @@ GEM ...@@ -918,7 +918,7 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.3.0) unicode-display_width (1.3.2)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
...@@ -1036,7 +1036,7 @@ DEPENDENCIES ...@@ -1036,7 +1036,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.99.0) gitaly-proto (~> 0.100.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1129,7 +1129,7 @@ DEPENDENCIES ...@@ -1129,7 +1129,7 @@ DEPENDENCIES
recaptcha (~> 3.0) recaptcha (~> 3.0)
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
......
...@@ -48,11 +48,10 @@ export default { ...@@ -48,11 +48,10 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`; return `${this.job.runner.description} (#${this.job.runner.id})`;
}, },
retryButtonClass() { retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; let className =
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className += className +=
this.job.status && this.job.recoverable this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
? ' btn-primary'
: ' btn-inverted-secondary';
return className; return className;
}, },
hasTimeout() { hasTimeout() {
...@@ -104,8 +103,7 @@ export default { ...@@ -104,8 +103,7 @@ export default {
<button <button
type="button" type="button"
:aria-label="__('Toggle Sidebar')" :aria-label="__('Toggle Sidebar')"
class="btn btn-blank gutter-toggle pull-right class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
d-block d-sm-block d-md-none js-sidebar-build-toggle"
> >
<i <i
aria-hidden="true" aria-hidden="true"
......
import groupAvatar from '~/group_avatar'; import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown'; import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal'; import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
groupAvatar(); groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal(); initConfirmDangerModal();
}); });
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
});
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
document.addEventListener('DOMContentLoaded', () => {
initGkeDropdowns();
});
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility'; import { slugify } from '../../../lib/utils/text_utility';
import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
...@@ -28,7 +30,12 @@ export default class Wikis { ...@@ -28,7 +30,12 @@ export default class Wikis {
if (slug.length > 0) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path'); const wikisPath = slugInput.getAttribute('data-wikis-path');
window.location.href = `${wikisPath}/${slug}`;
// If the wiki is empty, we need to merge the current URL params to keep the "create" view.
const params = parseQueryStringIntoObject(window.location.search.substr(1));
const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
redirectTo(url);
e.preventDefault(); e.preventDefault();
} }
} }
......
import _ from 'underscore';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import store from '../store';
export default {
store,
components: {
LoadingIcon,
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
},
props: {
fieldId: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
defaultValue: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isLoading: false,
hasErrors: false,
searchQuery: '',
gapiError: '',
};
},
computed: {
results() {
if (!this.items) {
return [];
}
return this.items.filter(item => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
},
},
methods: {
fetchSuccessHandler() {
if (this.defaultValue) {
const itemToSelect = _.find(this.items, item => item.name === this.defaultValue);
if (itemToSelect) {
this.setItem(itemToSelect.name);
}
}
this.isLoading = false;
this.hasErrors = false;
},
fetchFailureHandler(resp) {
this.isLoading = false;
this.hasErrors = true;
if (resp.result && resp.result.error) {
this.gapiError = resp.result.error.message;
}
},
},
};
<script>
import { sprintf, s__ } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeMachineTypeDropdown',
mixins: [gkeDropdownMixin],
computed: {
...mapState([
'isValidatingProjectBilling',
'projectHasBillingEnabled',
'selectedZone',
'selectedMachineType',
]),
...mapState({ items: 'machineTypes' }),
...mapGetters(['hasZone', 'hasMachineType']),
allDropdownsSelected() {
return this.projectHasBillingEnabled && this.hasZone && this.hasMachineType;
},
isDisabled() {
return (
this.isLoading ||
this.isValidatingProjectBilling ||
!this.projectHasBillingEnabled ||
!this.hasZone
);
},
toggleText() {
if (this.isLoading) {
return s__('ClusterIntegration|Fetching machine types');
}
if (this.selectedMachineType) {
return this.selectedMachineType;
}
if (!this.projectHasBillingEnabled && !this.hasZone) {
return s__('ClusterIntegration|Select project and zone to choose machine type');
}
return !this.hasZone
? s__('ClusterIntegration|Select zone to choose machine type')
: s__('ClusterIntegration|Select machine type');
},
errorMessage() {
return sprintf(
s__(
'ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}',
),
{ error: this.gapiError },
);
},
},
watch: {
selectedZone() {
this.hasErrors = false;
if (this.hasZone) {
this.isLoading = true;
this.fetchMachineTypes()
.then(this.fetchSuccessHandler)
.catch(this.fetchFailureHandler);
}
},
selectedMachineType() {
this.enableSubmit();
},
},
methods: {
...mapActions(['fetchMachineTypes']),
...mapActions({ setItem: 'setMachineType' }),
enableSubmit() {
if (this.allDropdownsSelected) {
const submitButtonEl = document.querySelector('.js-gke-cluster-creation-submit');
if (submitButtonEl) {
submitButtonEl.removeAttribute('disabled');
}
}
},
},
};
</script>
<template>
<div>
<div
class="js-gcp-machine-type-dropdown dropdown"
:class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedMachineType"
/>
<dropdown-button
:class="{ 'gl-field-error-outline': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search machine types')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No machine types matched your search') }}
</span>
</li>
<li
v-for="result in results"
:key="result.id"
>
<button
type="button"
@click.prevent="setItem(result.name)"
>
{{ result.name }}
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
<span
class="help-block"
:class="{ 'gl-field-error': hasErrors }"
v-if="hasErrors"
>
{{ errorMessage }}
</span>
</div>
</template>
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeProjectIdDropdown',
mixins: [gkeDropdownMixin],
props: {
docsUrl: {
type: String,
required: true,
},
},
computed: {
...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
...mapState({ items: 'projects' }),
...mapGetters(['hasProject']),
hasOneProject() {
return this.items && this.items.length === 1;
},
isDisabled() {
return (
this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
);
},
toggleText() {
if (this.isValidatingProjectBilling) {
return s__('ClusterIntegration|Validating project billing status');
}
if (this.isLoading) {
return s__('ClusterIntegration|Fetching projects');
}
if (this.hasProject) {
return this.selectedProject.name;
}
if (!this.items) {
return s__('ClusterIntegration|No projects found');
}
return s__('ClusterIntegration|Select project');
},
helpText() {
let message;
if (this.hasErrors) {
return this.errorMessage;
}
if (!this.items) {
message =
'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
}
message =
this.items && this.items.length
? 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.'
: 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
return sprintf(
s__(message),
{
docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
docsLinkStart: `<a href="${_.escape(
this.docsUrl,
)}" target="_blank" rel="noopener noreferrer">`,
},
false,
);
},
errorMessage() {
if (!this.projectHasBillingEnabled) {
if (this.gapiError) {
return s__(
'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
);
}
return sprintf(
s__(
'This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target="_blank" rel="noopener noreferrer">enable billing <i class="fa fa-external-link" aria-hidden="true"></i></a> and try again.',
),
{
linkToBilling:
'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral',
},
false,
);
}
return sprintf(
s__('ClusterIntegration|An error occured while trying to fetch your projects: %{error}'),
{ error: this.gapiError },
);
},
},
watch: {
selectedProject() {
this.setIsValidatingProjectBilling(true);
this.validateProjectBilling()
.then(this.validateProjectBillingSuccessHandler)
.catch(this.validateProjectBillingFailureHandler);
},
},
created() {
this.isLoading = true;
this.fetchProjects()
.then(this.fetchSuccessHandler)
.catch(this.fetchFailureHandler);
},
methods: {
...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
...mapActions({ setItem: 'setProject' }),
fetchSuccessHandler() {
if (this.defaultValue) {
const projectToSelect = _.find(this.items, item => item.projectId === this.defaultValue);
if (projectToSelect) {
this.setItem(projectToSelect);
}
} else if (this.items.length === 1) {
this.setItem(this.items[0]);
}
this.isLoading = false;
this.hasErrors = false;
},
validateProjectBillingSuccessHandler() {
this.hasErrors = !this.projectHasBillingEnabled;
},
validateProjectBillingFailureHandler(resp) {
this.hasErrors = true;
this.gapiError = resp.result ? resp.result.error.message : resp;
},
},
};
</script>
<template>
<div>
<div
class="js-gcp-project-id-dropdown dropdown"
:class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedProject.projectId"
/>
<dropdown-button
:class="{
'gl-field-error-outline': hasErrors,
'read-only': hasOneProject
}"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search projects')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No projects matched your search') }}
</span>
</li>
<li
v-for="result in results"
:key="result.project_number"
>
<button
type="button"
@click.prevent="setItem(result)"
>
{{ result.name }}
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
<span
class="help-block"
:class="{ 'gl-field-error': hasErrors }"
v-html="helpText"
></span>
</div>
</template>
<script>
import { sprintf, s__ } from '~/locale';
import { mapState, mapActions } from 'vuex';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeZoneDropdown',
mixins: [gkeDropdownMixin],
computed: {
...mapState([
'selectedProject',
'selectedZone',
'projects',
'isValidatingProjectBilling',
'projectHasBillingEnabled',
]),
...mapState({ items: 'zones' }),
isDisabled() {
return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
},
toggleText() {
if (this.isLoading) {
return s__('ClusterIntegration|Fetching zones');
}
if (this.selectedZone) {
return this.selectedZone;
}
return !this.projectHasBillingEnabled
? s__('ClusterIntegration|Select project to choose zone')
: s__('ClusterIntegration|Select zone');
},
errorMessage() {
return sprintf(
s__('ClusterIntegration|An error occured while trying to fetch project zones: %{error}'),
{ error: this.gapiError },
);
},
},
watch: {
isValidatingProjectBilling(isValidating) {
this.hasErrors = false;
if (!isValidating && this.projectHasBillingEnabled) {
this.isLoading = true;
this.fetchZones()
.then(this.fetchSuccessHandler)
.catch(this.fetchFailureHandler);
}
},
},
methods: {
...mapActions(['fetchZones']),
...mapActions({ setItem: 'setZone' }),
},
};
</script>
<template>
<div>
<div
class="js-gcp-zone-dropdown dropdown"
:class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedZone"
/>
<dropdown-button
:class="{ 'gl-field-error-outline': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search zones')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No zones matched your search') }}
</span>
</li>
<li
v-for="result in results"
:key="result.id"
>
<button
type="button"
@click.prevent="setItem(result.name)"
>
{{ result.name }}
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
<span
class="help-block"
:class="{ 'gl-field-error': hasErrors }"
v-if="hasErrors"
>
{{ errorMessage }}
</span>
</div>
</template>
import { s__ } from '~/locale';
export const GCP_API_ERROR = s__(
'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
);
export const GCP_API_CLOUD_BILLING_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
export const GCP_API_COMPUTE_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';
/* global gapi */
import Vue from 'vue';
import Flash from '~/flash';
import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
import * as CONSTANTS from './constants';
const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
const el = document.querySelector(entryPoint);
if (!el) return false;
const hiddenInput = el.querySelector('input');
return new Vue({
el,
components: {
[componentName]: component,
},
render: createElement =>
createElement(componentName, {
props: {
fieldName: hiddenInput.getAttribute('name'),
fieldId: hiddenInput.getAttribute('id'),
defaultValue: hiddenInput.value,
...extraProps,
},
}),
});
};
const mountGkeProjectIdDropdown = () => {
const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
const el = document.querySelector(entryPoint);
mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
docsUrl: el.dataset.docsurl,
});
};
const mountGkeZoneDropdown = () => {
mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
};
const mountGkeMachineTypeDropdown = () => {
mountComponent(
'.js-gcp-machine-type-dropdown-entry-point',
GkeMachineTypeDropdown,
'gke-machine-type-dropdown',
);
};
const gkeDropdownErrorHandler = () => {
Flash(CONSTANTS.GCP_API_ERROR);
};
const initializeGapiClient = () => {
const el = document.querySelector('.js-gke-cluster-creation');
if (!el) return false;
return gapi.client
.init({
discoveryDocs: [
CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
],
})
.then(() => {
gapi.client.setToken({ access_token: el.dataset.token });
mountGkeProjectIdDropdown();
mountGkeZoneDropdown();
mountGkeMachineTypeDropdown();
})
.catch(gkeDropdownErrorHandler);
};
const initGkeDropdowns = () => {
if (!gapi) {
gkeDropdownErrorHandler();
return false;
}
return gapi.load('client', initializeGapiClient);
};
export default initGkeDropdowns;
/* global gapi */
import * as types from './mutation_types';
const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
new Promise((resolve, reject) => {
const request = resource.list(params);
return request.then(
resp => {
const { result } = resp;
commit(mutation, result[payloadKey]);
resolve();
},
resp => {
reject(resp);
},
);
});
export const setProject = ({ commit }, selectedProject) => {
commit(types.SET_PROJECT, selectedProject);
};
export const setZone = ({ commit }, selectedZone) => {
commit(types.SET_ZONE, selectedZone);
};
export const setMachineType = ({ commit }, selectedMachineType) => {
commit(types.SET_MACHINE_TYPE, selectedMachineType);
};
export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
};
export const fetchProjects = ({ commit }) =>
gapiResourceListRequest({
resource: gapi.client.cloudresourcemanager.projects,
params: {},
commit,
mutation: types.SET_PROJECTS,
payloadKey: 'projects',
});
export const validateProjectBilling = ({ dispatch, commit, state }) =>
new Promise((resolve, reject) => {
const request = gapi.client.cloudbilling.projects.getBillingInfo({
name: `projects/${state.selectedProject.projectId}`,
});
commit(types.SET_ZONE, '');
commit(types.SET_MACHINE_TYPE, '');
return request.then(
resp => {
const { billingEnabled } = resp.result;
commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
dispatch('setIsValidatingProjectBilling', false);
resolve();
},
resp => {
dispatch('setIsValidatingProjectBilling', false);
reject(resp);
},
);
});
export const fetchZones = ({ commit, state }) =>
gapiResourceListRequest({
resource: gapi.client.compute.zones,
params: {
project: state.selectedProject.projectId,
},
commit,
mutation: types.SET_ZONES,
payloadKey: 'items',
});
export const fetchMachineTypes = ({ commit, state }) =>
gapiResourceListRequest({
resource: gapi.client.compute.machineTypes,
params: {
project: state.selectedProject.projectId,
zone: state.selectedZone,
},
commit,
mutation: types.SET_MACHINE_TYPES,
payloadKey: 'items',
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const hasProject = state => !!state.selectedProject.projectId;
export const hasZone = state => !!state.selectedZone;
export const hasMachineType = state => !!state.selectedMachineType;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state: createState(),
});
export default createStore();
export const SET_PROJECT = 'SET_PROJECT';
export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
export const SET_ZONE = 'SET_ZONE';
export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
export const SET_PROJECTS = 'SET_PROJECTS';
export const SET_ZONES = 'SET_ZONES';
export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';
import * as types from './mutation_types';
export default {
[types.SET_PROJECT](state, selectedProject) {
Object.assign(state, { selectedProject });
},
[types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
Object.assign(state, { isValidatingProjectBilling });
},
[types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
Object.assign(state, { projectHasBillingEnabled });
},
[types.SET_ZONE](state, selectedZone) {
Object.assign(state, { selectedZone });
},
[types.SET_MACHINE_TYPE](state, selectedMachineType) {
Object.assign(state, { selectedMachineType });
},
[types.SET_PROJECTS](state, projects) {
Object.assign(state, { projects });
},
[types.SET_ZONES](state, zones) {
Object.assign(state, { zones });
},
[types.SET_MACHINE_TYPES](state, machineTypes) {
Object.assign(state, { machineTypes });
},
};
export default () => ({
selectedProject: {
projectId: '',
name: '',
},
selectedZone: '',
selectedMachineType: '',
isValidatingProjectBilling: null,
projectHasBillingEnabled: null,
projects: [],
zones: [],
machineTypes: [],
});
<script>
import { __ } from '~/locale';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: {
LoadingIcon,
},
props: {
isDisabled: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
toggleText: {
type: String,
required: false,
default: __('Select'),
},
},
};
</script>
<template>
<button
class="dropdown-menu-toggle dropdown-menu-full-width"
type="button"
data-toggle="dropdown"
aria-expanded="false"
:disabled="isDisabled || isLoading"
>
<loading-icon
v-show="isLoading"
:inline="true"
/>
<span class="dropdown-toggle-text">
{{ toggleText }}
</span>
<span
class="dropdown-toggle-icon"
v-show="!isLoading"
>
<i
class="fa fa-chevron-down"
aria-hidden="true"
data-hidden="true"
></i>
</span>
</button>
</template>
...@@ -5,8 +5,8 @@ export default { ...@@ -5,8 +5,8 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
label: { value: {
type: Object, type: [Number, String],
required: true, required: true,
}, },
}, },
...@@ -17,6 +17,6 @@ export default { ...@@ -17,6 +17,6 @@ export default {
<input <input
type="hidden" type="hidden"
:name="name" :name="name"
:value="label.id" :value="value"
/> />
</template> </template>
<script>
import { __ } from '~/locale';
export default {
props: {
placeholderText: {
type: String,
required: true,
default: __('Search'),
},
},
data() {
return { searchQuery: this.value };
},
watch: {
searchQuery(query) {
this.$emit('input', query);
},
},
};
</script>
<template>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
v-model="searchQuery"
:placeholder="placeholderText"
autocomplete="off"
/>
<i
class="fa fa-search dropdown-input-search"
aria-hidden="true"
data-hidden="true"
>
</i>
<i
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
aria-hidden="true"
data-hidden="true"
role="button"
>
</i>
</div>
</template>
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
import $ from 'jquery'; import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import LoadingIcon from '../../loading_icon.vue'; import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue'; import DropdownButton from './dropdown_button.vue';
import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue'; import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue'; import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue'; import DropdownFooter from './dropdown_footer.vue';
...@@ -140,7 +140,7 @@ export default { ...@@ -140,7 +140,7 @@ export default {
v-for="label in context.labels" v-for="label in context.labels"
:key="label.id" :key="label.id"
:name="hiddenInputName" :name="hiddenInputName"
:label="label" :value="label.id"
/> />
<div <div
class="dropdown" class="dropdown"
......
...@@ -63,6 +63,10 @@ ...@@ -63,6 +63,10 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&:disabled.read-only {
color: $gl-text-color !important;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
@mixin flat-connector-before($length: 44px) {
&::before {
content: '';
position: absolute;
top: 48%;
left: -$length;
border-top: 2px solid $border-color;
width: $length;
height: 1px;
}
}
@mixin build-content($border-radius: 30px) {
display: inline-block;
padding: 8px 10px 9px;
width: 100%;
border: 1px solid $border-color;
border-radius: $border-radius;
background-color: $white-light;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
color: $gl-text-color;
}
}
.pipelines { .pipelines {
.stage { .stage {
max-width: 90px; max-width: 90px;
...@@ -357,14 +384,8 @@ ...@@ -357,14 +384,8 @@
&:not(:first-child) { &:not(:first-child) {
margin-left: 44px; margin-left: 44px;
.left-connector::before { .left-connector {
content: ''; @include flat-connector-before;
position: absolute;
top: 48%;
left: -44px;
border-top: 2px solid $border-color;
width: 44px;
height: 1px;
} }
} }
} }
...@@ -479,12 +500,7 @@ ...@@ -479,12 +500,7 @@
} }
.build-content { .build-content {
display: inline-block; @include build-content();
padding: 8px 10px 9px;
width: 100%;
border: 1px solid $border-color;
border-radius: 30px;
background-color: $white-light;
} }
a.build-content:hover, a.build-content:hover,
...@@ -622,8 +638,7 @@ ...@@ -622,8 +638,7 @@
} }
} }
// Dropdown button in mini pipeline graph @mixin mini-pipeline-item() {
button.mini-pipeline-graph-dropdown-toggle {
border-radius: 100px; border-radius: 100px;
background-color: $white-light; background-color: $white-light;
border-width: 1px; border-width: 1px;
...@@ -636,30 +651,6 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -636,30 +651,6 @@ button.mini-pipeline-graph-dropdown-toggle {
position: relative; position: relative;
vertical-align: middle; vertical-align: middle;
> .fa.fa-caret-down {
position: absolute;
left: 20px;
top: 5px;
display: inline-block;
visibility: hidden;
opacity: 0;
color: inherit;
font-size: 12px;
transition: visibility 0.1s, opacity 0.1s linear;
}
&:active,
&:focus,
&:hover {
outline: none;
width: 35px;
.fa.fa-caret-down {
visibility: visible;
opacity: 1;
}
}
// Dropdown button animation in mini pipeline graph // Dropdown button animation in mini pipeline graph
&.ci-status-icon-success { &.ci-status-icon-success {
@include mini-pipeline-graph-color($green-100, $green-500, $green-600); @include mini-pipeline-graph-color($green-100, $green-500, $green-600);
...@@ -691,6 +682,35 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -691,6 +682,35 @@ button.mini-pipeline-graph-dropdown-toggle {
} }
} }
// Dropdown button in mini pipeline graph
button.mini-pipeline-graph-dropdown-toggle {
@include mini-pipeline-item();
> .fa.fa-caret-down {
position: absolute;
left: 20px;
top: 5px;
display: inline-block;
visibility: hidden;
opacity: 0;
color: inherit;
font-size: 12px;
transition: visibility 0.1s, opacity 0.1s linear;
}
&:active,
&:focus,
&:hover {
outline: none;
width: 35px;
.fa.fa-caret-down {
visibility: visible;
opacity: 1;
}
}
}
/** /**
Action icons inside dropdowns: Action icons inside dropdowns:
- mini graph in pipelines table - mini graph in pipelines table
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
include CountHelper include CountHelper
COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
Note, Snippet, Key, Milestone].freeze
def index def index
@counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10) @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10) @users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10) @groups = Group.order_id_desc.with_route.limit(10)
......
class Projects::Clusters::GcpController < Projects::ApplicationController class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
before_action :authorize_google_api, except: [:login]
before_action :authorize_google_project_billing, only: [:new, :create]
before_action :authorize_create_cluster!, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :create]
before_action :verify_billing, only: [:create] before_action :authorize_google_api, except: :login
helper_method :token_in_session
def login def login
begin begin
...@@ -37,21 +36,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -37,21 +36,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
private private
def verify_billing
case google_project_billing_status
when nil
flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
when false
flash.now[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
when true
return
end
@cluster = ::Clusters::Cluster.new(create_params)
render :new
end
def create_params def create_params
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
...@@ -75,17 +59,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -75,17 +59,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
end end
def authorize_google_project_billing
redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session)
CheckGcpProjectBillingWorker.perform_async(redis_token_key)
end
def google_project_billing_status
CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
end
def token_in_session def token_in_session
@token_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_token] session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end end
......
...@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController
def show def show
@page = @project_wiki.find_page(params[:id], params[:version_id]) @page = @project_wiki.find_page(params[:id], params[:version_id])
view_param = @project_wiki.empty? ? params[:view] : 'create'
if @page if @page
render 'show' render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id]) elsif file = @project_wiki.find_file(params[:id], params[:version_id])
...@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController
disposition: 'inline', disposition: 'inline',
filename: file.name filename: file.name
) )
else elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = build_page(title: params[:id]) @page = build_page(title: params[:id])
render 'edit' render 'edit'
else
render 'empty'
end end
end end
......
...@@ -39,15 +39,6 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -39,15 +39,6 @@ class GroupProjectsFinder < ProjectsFinder
end end
def collection_with_user def collection_with_user
if group.users.include?(current_user)
if only_shared?
[shared_projects]
elsif only_owned?
[owned_projects]
else
[shared_projects, owned_projects]
end
else
if only_shared? if only_shared?
[shared_projects.public_or_visible_to_user(current_user)] [shared_projects.public_or_visible_to_user(current_user)]
elsif only_owned? elsif only_owned?
...@@ -59,7 +50,6 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -59,7 +50,6 @@ class GroupProjectsFinder < ProjectsFinder
] ]
end end
end end
end
def collection_without_user def collection_without_user
if only_shared? if only_shared?
......
module CountHelper module CountHelper
def approximate_count_with_delimiters(model) def approximate_count_with_delimiters(count_data, model)
number_with_delimiter(Gitlab::Database::Count.approximate_count(model)) count = count_data[model]
raise "Missing model #{model} from count data" unless count
number_with_delimiter(count)
end end
end end
...@@ -11,6 +11,7 @@ module NavHelper ...@@ -11,6 +11,7 @@ module NavHelper
class_name = page_gutter_class class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
class_name class_name
end end
......
...@@ -257,6 +257,9 @@ module ProjectsHelper ...@@ -257,6 +257,9 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project) if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines nav_tabs << :pipelines
end
if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
nav_tabs << :operations nav_tabs << :operations
end end
......
module DiffFile
extend ActiveSupport::Concern
def to_hash
keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
as_json(only: keys).merge(diff: diff).with_indifferent_access
end
end
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
# A note of this type can be resolvable. # A note of this type can be resolvable.
class DiffNote < Note class DiffNote < Note
include NoteOnDiff include NoteOnDiff
include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
...@@ -12,7 +13,6 @@ class DiffNote < Note ...@@ -12,7 +13,6 @@ class DiffNote < Note
validates :original_position, presence: true validates :original_position, presence: true
validates :position, presence: true validates :position, presence: true
validates :diff_line, presence: true, if: :on_text?
validates :line_code, presence: true, line_code: true, if: :on_text? validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete validate :positions_complete
...@@ -23,6 +23,7 @@ class DiffNote < Note ...@@ -23,6 +23,7 @@ class DiffNote < Note
before_validation :update_position, on: :create, if: :on_text? before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text? before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits after_save :keep_around_commits
after_commit :create_diff_file, on: :create
def discussion_class(*) def discussion_class(*)
DiffDiscussion DiffDiscussion
...@@ -53,20 +54,24 @@ class DiffNote < Note ...@@ -53,20 +54,24 @@ class DiffNote < Note
position.position_type == "image" position.position_type == "image"
end end
def diff_file def create_diff_file
@diff_file ||= return unless should_create_diff_file?
begin
if created_at_diff?(noteable.diff_refs) diff_file = fetch_diff_file
# We're able to use the already persisted diffs (Postgres) if we're diff_line = diff_file.line_for_position(self.original_position)
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it. creation_params = diff_file.diff.to_hash
# As an extra benefit, the returned `diff_file` already .except(:too_large)
# has `highlighted_diff_lines` data set from Redis on .merge(diff: diff_file.diff_hunk(diff_line))
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first create_note_diff_file(creation_params)
else
original_position.diff_file(self.project.repository)
end end
def diff_file
strong_memoize(:diff_file) do
enqueue_diff_file_creation_job if should_create_diff_file?
fetch_diff_file
end end
end end
...@@ -98,6 +103,38 @@ class DiffNote < Note ...@@ -98,6 +103,38 @@ class DiffNote < Note
private private
def enqueue_diff_file_creation_job
# Avoid enqueuing multiple file creation jobs at once for a note (i.e.
# parallel calls to `DiffNote#diff_file`).
lease = Gitlab::ExclusiveLease.new("note_diff_file_creation:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain
CreateNoteDiffFileWorker.perform_async(id)
end
def should_create_diff_file?
on_text? && note_diff_file.nil? && self == discussion.first_note
end
def fetch_diff_file
if note_diff_file
diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
Gitlab::Diff::File.new(diff,
repository: project.repository,
diff_refs: original_position.diff_refs)
elsif created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it.
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
else
original_position.diff_file(self.project.repository)
end
end
def supported? def supported?
for_commit? || self.noteable.has_complete_diff_refs? for_commit? || self.noteable.has_complete_diff_refs?
end end
......
...@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base ...@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base
# #
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently. # As such, the increment is atomic and safe to be called concurrently.
# def increment_and_save!
# If a `maximum_iid` is passed in, this overrides the incremented value if it's
# greater than that. This can be used to correct the increment value if necessary.
def increment_and_save!(maximum_iid)
lock! lock!
self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max self.last_value = (last_value || 0) + 1
save! save!
last_value last_value
end end
...@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base ...@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base
# and increment its last value # and increment its last value
# #
# Note this will acquire a ROW SHARE lock on the InternalId record # Note this will acquire a ROW SHARE lock on the InternalId record
(lookup || create_record).increment_and_save!
# Note we always calculate the maximum iid present here and
# pass it in to correct the InternalId entry if it's last_value is off.
#
# This can happen in a transition phase where both `AtomicInternalId` and
# `NonatomicInternalId` code runs (e.g. during a deploy).
#
# This is subject to be cleaned up with the 10.8 release:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
(lookup || create_record).increment_and_save!(maximum_iid)
end end
end end
...@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base ...@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
last_value: maximum_iid last_value: init.call(subject) || 0
) )
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
lookup lookup
end end
def maximum_iid
@maximum_iid ||= init.call(subject) || 0
end
end end
end end
class MergeRequestDiffFile < ActiveRecord::Base class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
include DiffFile
belongs_to :merge_request_diff belongs_to :merge_request_diff
...@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base ...@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base
def diff def diff
binary? ? super.unpack('m0').first : super binary? ? super.unpack('m0').first : super
end end
def to_hash
keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
as_json(only: keys).merge(diff: diff).with_indifferent_access
end
end end
...@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base ...@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base
has_many :todos has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
delegate :gfm_reference, :local_reference, to: :noteable delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
...@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base ...@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) } scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) } scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do scope :inc_relations_for_view, -> do
includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata) includes(:project, :author, :updated_by, :resolved_by, :award_emoji,
:system_note_metadata, :note_diff_file)
end end
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) } scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
......
class NoteDiffFile < ActiveRecord::Base
include DiffFile
belongs_to :diff_note, inverse_of: :note_diff_file
validates :diff_note, presence: true
end
class CheckGcpProjectBillingService
def execute(token)
client = GoogleApi::CloudPlatform::Client.new(token, nil)
client.projects_list.select do |project|
begin
client.projects_get_billing_info(project.project_id).billing_enabled
rescue
end
end
end
end
...@@ -11,7 +11,7 @@ module ObjectStorage ...@@ -11,7 +11,7 @@ module ObjectStorage
ObjectStorageUnavailable = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError)
DIRECT_UPLOAD_TIMEOUT = 4.hours DIRECT_UPLOAD_TIMEOUT = 4.hours
TMP_UPLOAD_PATH = 'tmp/upload'.freeze TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store module Store
LOCAL = 1 LOCAL = 1
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
.form-check .form-check
= level = level
%span.form-text.text-muted#restricted-visibility-help %span.form-text.text-muted#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets. Selected levels cannot be used by non-admin users for groups, projects or snippets.
If the public level is restricted, user profiles are only visible to logged in users. If the public level is restricted, user profiles are only visible to logged in users.
.form-group.row .form-group.row
= f.label :import_sources, class: 'col-form-label col-sm-2' = f.label :import_sources, class: 'col-form-label col-sm-2'
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= link_to admin_projects_path do = link_to admin_projects_path do
%h3.text-center %h3.text-center
Projects: Projects:
= approximate_count_with_delimiters(Project) = approximate_count_with_delimiters(@counts, Project)
%hr %hr
= link_to('New project', new_project_path, class: "btn btn-new") = link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4 .col-sm-4
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= link_to admin_users_path do = link_to admin_users_path do
%h3.text-center %h3.text-center
Users: Users:
= approximate_count_with_delimiters(User) = approximate_count_with_delimiters(@counts, User)
= render_if_exists 'users_statistics' = render_if_exists 'users_statistics'
%hr %hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new" = link_to 'New user', new_admin_user_path, class: "btn btn-new"
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
= link_to admin_groups_path do = link_to admin_groups_path do
%h3.text-center %h3.text-center
Groups: Groups:
= approximate_count_with_delimiters(Group) = approximate_count_with_delimiters(@counts, Group)
%hr %hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new" = link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row .row
...@@ -42,31 +42,31 @@ ...@@ -42,31 +42,31 @@
%p %p
Forks Forks
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(ForkedProjectLink) = approximate_count_with_delimiters(@counts, ForkedProjectLink)
%p %p
Issues Issues
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Issue) = approximate_count_with_delimiters(@counts, Issue)
%p %p
Merge Requests Merge Requests
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(MergeRequest) = approximate_count_with_delimiters(@counts, MergeRequest)
%p %p
Notes Notes
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Note) = approximate_count_with_delimiters(@counts, Note)
%p %p
Snippets Snippets
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Snippet) = approximate_count_with_delimiters(@counts, Snippet)
%p %p
SSH Keys SSH Keys
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Key) = approximate_count_with_delimiters(@counts, Key)
%p %p
Milestones Milestones
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Milestone) = approximate_count_with_delimiters(@counts, Milestone)
%p %p
Active Users Active Users
%span.light.float-right %span.light.float-right
......
- breadcrumb_title "General Settings" - breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- expanded = Rails.env.test?
.card.prepend-top-default
.card-header
Group settings
.card-body
= form_for @group, html: { multipart: true, class: "gl-show-field-errors" }, authenticity_token: true do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
.form-group.row %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) }
.offset-sm-2.col-sm-10 .settings-header
.avatar-container.s160 %h4
= group_icon(@group, alt: '', class: 'avatar group-avatar s160') = _('General')
%p.light %button.btn.js-settings-toggle{ type: 'button' }
- if @group.avatar? = expanded ? _('Collapse') : _('Expand')
You can change the group avatar here
- else
You can upload a group avatar here
= render 'shared/choose_group_avatar_button', f: f
- if @group.avatar?
%hr
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _("Avatar will be removed. Are you sure?")}, method: :delete, class: "btn btn-danger btn-inverted"
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group.row
.offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
.form-group.row
%label.col-form-label.col-sm-2
= s_("GroupSettings|Share with group lock")
.col-sm-10
.form-check
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'group_admin_settings', f: f
.form-actions
= f.submit 'Save group', class: "btn btn-save"
.card.bg-danger
.card-header Remove group
.card-body
= form_tag(@group, method: :delete) do
%p %p
Removing group will cause all child projects and resources to be removed. = _('Update your group name, description, avatar, and other general settings.')
%br .settings-content
%strong Removed group can not be restored! = render 'groups/settings/general'
.form-actions %section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) }
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) } .settings-header
%h4
- if supports_nested_groups? = _('Permissions')
.card.bg-warning %button.btn.js-settings-toggle{ type: 'button' }
.card-header Transfer group = expanded ? _('Collapse') : _('Expand')
.card-body %p
= form_for @group, url: transfer_group_path(@group), method: :put do |f| = _('Enable or disable certain group features and choose access levels.')
.form-group .settings-content
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } }) = render 'groups/settings/permissions'
= hidden_field_tag 'new_parent_group_id'
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
%ul .settings-header
%li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}. %h4
%li You can only transfer the group to a group you manage. = _('Advanced')
%li You will need to update your local repositories to point to the new location. %button.btn.js-settings-toggle{ type: 'button' }
%li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility. = expanded ? _('Collapse') : _('Expand')
= f.submit 'Transfer group', class: "btn btn-warning" %p
= _('Perform advanced options such as changing path, transferring, or removing the group.')
.settings-content
= render 'groups/settings/advanced'
= render 'shared/confirm_modal', phrase: @group.path = render 'shared/confirm_modal', phrase: @group.path
.sub-section
%h4.warning-title Change group path
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
.form-group
%p
Changing group path can have unintended side effects.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
.input-group.gl-field-error-anchor
.group-root-path.input-group-prepend.has-tooltip{ title: group_path(@group), :'data-placement' => 'bottom' }
.input-group-text
%span>= root_url
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
= f.submit 'Change group path', class: 'btn btn-warning'
.sub-section
%h4.danger-title Remove group
= form_tag(@group, method: :delete) do
%p
Removing group will cause all child projects and resources to be removed.
%br
%strong Removed group can not be restored!
= button_to 'Remove group', '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) }
- if supports_nested_groups?
.sub-section
%h4.warning-title Transfer group
= form_for @group, url: transfer_group_path(@group), method: :put do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: 'Search groups', data: { data: parent_group_options(@group) } })
= hidden_field_tag 'new_parent_group_id'
%ul
%li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
%li You can only transfer the group to a group you manage.
%li You will need to update your local repositories to point to the new location.
%li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
= f.submit 'Transfer group', class: 'btn btn-warning'
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
%fieldset
.row
.form-group.col-md-9
= f.label :name, class: 'label-light' do
Group name
= f.text_field :name, class: 'form-control'
.form-group.col-md-3
= f.label :id, class: 'label-light' do
Group ID
= f.text_field :id, class: 'form-control', readonly: true
.form-group
= f.label :description, class: 'label-light' do
Group description
%span.light (optional)
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
.form-group.row
.col-sm-12
.avatar-container.s160
= group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
You can change the group avatar here
- else
You can upload a group avatar here
= render 'shared/choose_group_avatar_button', f: f
- if @group.avatar?
%hr
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted'
= f.submit 'Save group', class: 'btn btn-success'
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
%fieldset
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group.row
.offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
.form-group.row
%label.col-form-label.col-sm-2
= s_('GroupSettings|Share with group lock')
.col-sm-10
.form-check
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'groups/group_admin_settings', f: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
= f.submit 'Save group', class: 'btn btn-success'
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) } %section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _('Secret variables') = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" } %button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
......
= javascript_include_tag 'https://apis.google.com/js/api.js'
%p %p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name') = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
...@@ -14,13 +16,25 @@ ...@@ -14,13 +16,25 @@
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group .form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
= link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID') = provider_gcp_field.hidden_field :gcp_project_id
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project')
= icon('chevron-down')
%span.help-block &nbsp;
.form-group .form-group
= provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
= link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
= provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a' .js-gcp-zone-dropdown-entry-point
= provider_gcp_field.hidden_field :zone
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project to choose zone')
= icon('chevron-down')
.form-group .form-group
= provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
...@@ -28,8 +42,13 @@ ...@@ -28,8 +42,13 @@
.form-group .form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
= link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') .js-gcp-machine-type-dropdown-entry-point
= provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' = provider_gcp_field.hidden_field :machine_type
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project and zone to choose machine type')
= icon('chevron-down')
.form-group .form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _('Secret variables') = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
......
- page_title _("Wiki") - page_title _("Wiki")
- @right_sidebar = false
%h3.page-title= s_("Wiki|Empty page") = render 'shared/empty_states/wikis'
%hr
.error_message
= s_("WikiEmptyPageError|You are not allowed to create wiki pages")
- layout_path = 'shared/empty_states/wikis_layout'
- if can?(current_user, :create_wiki, @project)
- create_path = project_wiki_path(@project, params[:id], { view: 'create' })
- create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-new', title: s_('WikiEmpty|Create your first page')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4
= s_('WikiEmpty|The wiki lets you write documentation for your project')
%p.text-left
= s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, it's principles, how to use it, and so on.")
= create_link
- elsif can?(current_user, :read_issue, @project)
- issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project)
- new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-new', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
%h4
= s_('WikiEmpty|This project has no wiki pages')
%p.text-left
= s_('WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}.').html_safe % { issues_link: issues_link }
= new_issue_link
- else
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
%h4
= s_('WikiEmpty|This project has no wiki pages')
%p
= s_('WikiEmpty|You must be a project member in order to add wiki pages.')
.row.empty-state
.col-xs-12
.svg-content
= image_tag image_path
.col-xs-12
.text-content.text-center
= yield
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
%span= milestone.issues_visible_to_user(current_user).count %span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed .title.hide-collapsed
Issues Issues
%span.badg.badge-pille= milestone.issues_visible_to_user(current_user).count %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).count
- if show_new_issue_link?(project) - if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: "New Issue" do = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: "New Issue" do
New issue New issue
...@@ -99,6 +99,8 @@ ...@@ -99,6 +99,8 @@
= _('Time tracking') = _('Time tracking')
= icon('spinner spin') = icon('spinner spin')
= render_if_exists 'shared/milestones/weight', milestone: milestone
.block.merge-requests .block.merge-requests
.sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%strong %strong
......
...@@ -48,6 +48,8 @@ ...@@ -48,6 +48,8 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg} %span All issues for this milestone are closed. #{close_msg}
= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
- if is_dynamic_milestone - if is_dynamic_milestone
.table-holder .table-holder
%table.table %table.table
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
- gcp_cluster:cluster_provision - gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation - gcp_cluster:wait_for_cluster_creation
- gcp_cluster:check_gcp_project_billing
- gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage - github_import_advance_stage
...@@ -115,3 +114,4 @@ ...@@ -115,3 +114,4 @@
- upload_checksum - upload_checksum
- web_hook - web_hook
- repository_update_remote_mirror - repository_update_remote_mirror
- create_note_diff_file
require 'securerandom'
class CheckGcpProjectBillingWorker
include ApplicationWorker
include ClusterQueue
LEASE_TIMEOUT = 3.seconds.to_i
SESSION_KEY_TIMEOUT = 5.minutes
BILLING_TIMEOUT = 1.hour
BILLING_CHANGED_LABELS = { state_transition: nil }.freeze
def self.get_session_token(token_key)
Gitlab::Redis::SharedState.with do |redis|
redis.get(get_redis_session_key(token_key))
end
end
def self.store_session_token(token)
generate_token_key.tap do |token_key|
Gitlab::Redis::SharedState.with do |redis|
redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT)
end
end
end
def self.get_billing_state(token)
Gitlab::Redis::SharedState.with do |redis|
value = redis.get(redis_shared_state_key_for(token))
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
end
end
def perform(token_key)
return unless token_key
token = self.class.get_session_token(token_key)
return unless token
return unless try_obtain_lease_for(token)
billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty?
update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state)
self.class.set_billing_state(token, billing_enabled_state)
end
private
def self.generate_token_key
SecureRandom.uuid
end
def self.get_redis_session_key(token_key)
"gitlab:gcp:session:#{token_key}"
end
def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
end
def self.set_billing_state(token, value)
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT)
end
end
def try_obtain_lease_for(token)
Gitlab::ExclusiveLease
.new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
.try_obtain
end
def billing_changed_counter
@billing_changed_counter ||= Gitlab::Metrics.counter(
:gcp_billing_change_count,
"Counts the number of times a GCP project changed billing_enabled state from false to true",
BILLING_CHANGED_LABELS
)
end
def state_transition(previous_state, current_state)
if previous_state.nil? && !current_state
'no_billing'
elsif previous_state.nil? && current_state
'with_billing'
elsif !previous_state && current_state
'billing_configured'
end
end
def update_billing_change_counter(previous_state, current_state)
billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state))
end
end
class CreateNoteDiffFileWorker
include ApplicationWorker
def perform(diff_note_id)
diff_note = DiffNote.find(diff_note_id)
diff_note.create_diff_file
end
end
---
title: Dynamically fetch GCP cluster creation parameters.
merge_request: 17806
author:
type: changed
---
title: Add helpful messages to empty wiki view
merge_request: 19007
author:
type: other
---
title: Persist truncated note diffs on a new table
merge_request:
author:
type: performance
---
title: Update redis-namespace to 1.6.0
merge_request: 19166
author: Takuya Noguchi
type: other
---
title: Remove double-checked internal id generation.
merge_request: 19181
author:
type: performance
---
title: Expose artifacts_expire_at field for job entity in api
merge_request: 18872
author: Semyon Pupkov
type: added
---
title: Add backgound migration for filling nullfied file_store columns
merge_request: 18557
author:
type: performance
---
title: Add username to terms message in git and API calls
merge_request: 19126
author:
type: changed
---
title: Redesign group settings page into expandable sections
merge_request: 19184
author:
type: changed
---
title: Fix remote mirror database inconsistencies when upgrading from EE to CE
merge_request: 19196
author:
type: fixed
---
title: Improve performance of GroupsController#show
merge_request:
author:
type: performance
---
title: Disallow updating job status if the job is not running
merge_request: 19101
author:
type: fixed
---
title: Fix FreeBSD can not upload artifacts due to wrong tmp path
merge_request: 19148
author:
type: fixed
---
title: Fix admin counters not working when PostgreSQL has secondaries
merge_request:
author:
type: fixed
---
title: Log Workhorse queue duration for Grape API calls
merge_request:
author:
type: other
...@@ -75,4 +75,5 @@ ...@@ -75,4 +75,5 @@
- [pipeline_background, 1] - [pipeline_background, 1]
- [repository_update_remote_mirror, 1] - [repository_update_remote_mirror, 1]
- [repository_remove_remote, 1] - [repository_remove_remote, 1]
- [create_note_diff_file, 1]
class CreateNotesDiffFiles < ActiveRecord::Migration
DOWNTIME = false
disable_ddl_transaction!
def change
create_table :note_diff_files do |t|
t.references :diff_note, references: :notes, null: false, index: { unique: true }
t.text :diff, null: false
t.boolean :new_file, null: false
t.boolean :renamed_file, null: false
t.boolean :deleted_file, null: false
t.string :a_mode, null: false
t.string :b_mode, null: false
t.text :new_path, null: false
t.text :old_path, null: false
end
add_foreign_key :note_diff_files, :notes, column: :diff_note_id, on_delete: :cascade
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsTargetIdIidStatePartialIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
INDEX_NAME = 'index_merge_requests_on_target_project_id_and_iid_opened'
disable_ddl_transaction!
def up
# On GitLab.com this index will take up roughly 5 MB of space.
add_concurrent_index(
:merge_requests,
[:target_project_id, :iid],
where: "state = 'opened'",
name: INDEX_NAME
)
end
def down
remove_concurrent_index_by_name(:merge_requests, INDEX_NAME)
end
end
class EnsureRemoteMirrorColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :remote_mirrors, :last_update_started_at, :datetime unless column_exists?(:remote_mirrors, :last_update_started_at)
add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name)
unless column_exists?(:remote_mirrors, :only_protected_branches)
add_column_with_default(:remote_mirrors,
:only_protected_branches,
:boolean,
default: false,
allow_null: false)
end
end
def down
# db/migrate/20180503131624_create_remote_mirrors.rb will remove the table
end
end
class FillFileStore < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class JobArtifact < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_job_artifacts'
BATCH_SIZE = 10_000
def self.params_for_background_migration
yield self.where(file_store: nil), 'FillFileStoreJobArtifact', 5.minutes, BATCH_SIZE
end
end
class LfsObject < ActiveRecord::Base
include EachBatch
self.table_name = 'lfs_objects'
BATCH_SIZE = 10_000
def self.params_for_background_migration
yield self.where(file_store: nil), 'FillFileStoreLfsObject', 5.minutes, BATCH_SIZE
end
end
class Upload < ActiveRecord::Base
include EachBatch
self.table_name = 'uploads'
self.inheritance_column = :_type_disabled # Disable STI
BATCH_SIZE = 10_000
def self.params_for_background_migration
yield self.where(store: nil), 'FillStoreUpload', 5.minutes, BATCH_SIZE
end
end
def up
# NOTE: Schedule background migrations that fill 'NULL' value by '1'(ObjectStorage::Store::LOCAL) on `file_store`, `store` columns
#
# Here are the target columns
# - ci_job_artifacts.file_store
# - lfs_objects.file_store
# - uploads.store
FillFileStore::JobArtifact.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
queue_background_migration_jobs_by_range_at_intervals(relation,
class_name,
delay_interval,
batch_size: batch_size)
end
FillFileStore::LfsObject.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
queue_background_migration_jobs_by_range_at_intervals(relation,
class_name,
delay_interval,
batch_size: batch_size)
end
FillFileStore::Upload.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
queue_background_migration_jobs_by_range_at_intervals(relation,
class_name,
delay_interval,
batch_size: batch_size)
end
end
def down
# noop
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180521171529) do ActiveRecord::Schema.define(version: 20180529093006) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1232,6 +1232,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do ...@@ -1232,6 +1232,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
...@@ -1302,6 +1303,20 @@ ActiveRecord::Schema.define(version: 20180521171529) do ...@@ -1302,6 +1303,20 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "note_diff_files", force: :cascade do |t|
t.integer "diff_note_id", null: false
t.text "diff", null: false
t.boolean "new_file", null: false
t.boolean "renamed_file", null: false
t.boolean "deleted_file", null: false
t.string "a_mode", null: false
t.string "b_mode", null: false
t.text "new_path", null: false
t.text "old_path", null: false
end
add_index "note_diff_files", ["diff_note_id"], name: "index_note_diff_files_on_diff_note_id", unique: true, using: :btree
create_table "notes", force: :cascade do |t| create_table "notes", force: :cascade do |t|
t.text "note" t.text "note"
t.string "noteable_type" t.string "noteable_type"
...@@ -2243,6 +2258,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do ...@@ -2243,6 +2258,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
add_foreign_key "note_diff_files", "notes", column: "diff_note_id", on_delete: :cascade
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
......
...@@ -33,16 +33,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL: ...@@ -33,16 +33,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
# Disable all components except PostgreSQL # Disable all components except PostgreSQL
postgresql['enable'] = true roles ['postgres_role']
bootstrap['enable'] = false
nginx['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
redis['enable'] = false
prometheus['enable'] = false
gitaly['enable'] = false
gitlab_workhorse['enable'] = false
mailroom['enable'] = false
# PostgreSQL configuration # PostgreSQL configuration
gitlab_rails['db_password'] = 'DB password' gitlab_rails['db_password'] = 'DB password'
......
...@@ -47,7 +47,8 @@ for each GitLab application server in your environment. ...@@ -47,7 +47,8 @@ for each GitLab application server in your environment.
URL. Depending your the NFS configuration, you may need to change some GitLab URL. Depending your the NFS configuration, you may need to change some GitLab
data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb` data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb`
configuration values for various scenarios. The example below assumes you've configuration values for various scenarios. The example below assumes you've
added NFS mounts in the default data locations. added NFS mounts in the default data locations. Additionally the UID and GIDs
given are just examples and you should configure with your preferred values.
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
...@@ -68,6 +69,14 @@ for each GitLab application server in your environment. ...@@ -68,6 +69,14 @@ for each GitLab application server in your environment.
gitlab_rails['redis_port'] = '6379' gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server
gitlab_rails['redis_password'] = 'Redis Password' gitlab_rails['redis_password'] = 'Redis Password'
# Ensure UIDs and GIDs match between servers for permissions via NFS
user['uid'] = 9000
user['gid'] = 9000
web_server['uid'] = 9001
web_server['gid'] = 9001
registry['uid'] = 9002
registry['gid'] = 9002
``` ```
> **Note:** To maintain uniformity of links across HA clusters, the `external_url` > **Note:** To maintain uniformity of links across HA clusters, the `external_url`
...@@ -76,24 +85,23 @@ for each GitLab application server in your environment. ...@@ -76,24 +85,23 @@ for each GitLab application server in your environment.
In a typical HA setup, this will be the url of the load balancer which will In a typical HA setup, this will be the url of the load balancer which will
route traffic to all GitLab application servers in the HA cluster. route traffic to all GitLab application servers in the HA cluster.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration. > **Note:** When you specify `https` in the `external_url`, as in the example
above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
certificates are not present, Nginx will fail to start. See
[Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
for more information.
## First GitLab application server ## First GitLab application server
As a final step, run the setup rake task on the first GitLab application server. As a final step, run the setup rake task **only on** the first GitLab application server.
It is not necessary to run this on additional application servers. Do not run this on additional application servers.
1. Initialize the database by running `sudo gitlab-rake gitlab:setup`. 1. Initialize the database by running `sudo gitlab-rake gitlab:setup`.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
> **WARNING:** Only run this setup task on **NEW** GitLab instances because it > **WARNING:** Only run this setup task on **NEW** GitLab instances because it
will wipe any existing data. will wipe any existing data.
> **Note:** When you specify `https` in the `external_url`, as in the example
above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
certificates are not present, Nginx will fail to start. See
[Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
for more information.
## Extra configuration for additional GitLab application servers ## Extra configuration for additional GitLab application servers
Additional GitLab servers (servers configured **after** the first GitLab server) Additional GitLab servers (servers configured **after** the first GitLab server)
...@@ -101,8 +109,7 @@ need some extra configuration. ...@@ -101,8 +109,7 @@ need some extra configuration.
1. Configure shared secrets. These values can be obtained from the primary 1. Configure shared secrets. These values can be obtained from the primary
GitLab server in `/etc/gitlab/gitlab-secrets.json`. Add these to GitLab server in `/etc/gitlab/gitlab-secrets.json`. Add these to
`/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure` in `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure`.
the steps above.
```ruby ```ruby
gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860' gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860'
...@@ -115,6 +122,8 @@ need some extra configuration. ...@@ -115,6 +122,8 @@ need some extra configuration.
from running on upgrade. Only the primary GitLab application server should from running on upgrade. Only the primary GitLab application server should
handle migrations. handle migrations.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
## Troubleshooting ## Troubleshooting
- `mount: wrong fs type, bad option, bad superblock on` - `mount: wrong fs type, bad option, bad superblock on`
......
# Web terminals # Web terminals
> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can >
access web terminals. [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
in GitLab 8.15. Only project masters and owners can access web terminals.
With the introduction of the [Kubernetes project service][kubservice], GitLab With the introduction of the [Kubernetes integration](../../user/project/clusters/index.md),
gained the ability to store and use credentials for a Kubernetes cluster. One GitLab gained the ability to store and use credentials for a Kubernetes cluster.
of the things it uses these credentials for is providing access to One of the things it uses these credentials for is providing access to
[web terminals](../../ci/environments.html#web-terminals) for environments. [web terminals](../../ci/environments.md#web-terminals) for environments.
## How it works ## How it works
...@@ -80,6 +81,3 @@ Terminal sessions use long-lived connections; by default, these may last ...@@ -80,6 +81,3 @@ Terminal sessions use long-lived connections; by default, these may last
forever. You can configure a maximum session time in the Admin area of your forever. You can configure a maximum session time in the Admin area of your
GitLab instance if you find this undesirable from a scalability or security GitLab instance if you find this undesirable from a scalability or security
point of view. point of view.
[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690
[kubservice]: ../../user/project/integrations/kubernetes.md
...@@ -29,7 +29,8 @@ For installations from source you'll have to install and configure it yourself. ...@@ -29,7 +29,8 @@ For installations from source you'll have to install and configure it yourself.
Prometheus and it's exporters are on by default, starting with GitLab 9.0. Prometheus and it's exporters are on by default, starting with GitLab 9.0.
Prometheus will run as the `gitlab-prometheus` user and listen on Prometheus will run as the `gitlab-prometheus` user and listen on
`http://localhost:9090`. Each exporter will be automatically be set up as a `http://localhost:9090`. By default Prometheus is only accessible from the GitLab server itself.
Each exporter will be automatically set up as a
monitoring target for Prometheus, unless individually disabled. monitoring target for Prometheus, unless individually disabled.
To disable Prometheus and all of its exporters, as well as any added in the future: To disable Prometheus and all of its exporters, as well as any added in the future:
...@@ -44,14 +45,16 @@ To disable Prometheus and all of its exporters, as well as any added in the futu ...@@ -44,14 +45,16 @@ To disable Prometheus and all of its exporters, as well as any added in the futu
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect take effect
## Changing the port Prometheus listens on ## Changing the port and address Prometheus listens on
>**Note:** >**Note:**
The following change was added in [GitLab Omnibus 8.17][1261]. Although possible, The following change was added in [GitLab Omnibus 8.17][1261]. Although possible,
it's not recommended to change the default address and port Prometheus listens it's not recommended to change the port Prometheus listens
on as this might affect or conflict with other services running on the GitLab on as this might affect or conflict with other services running on the GitLab
server. Proceed at your own risk. server. Proceed at your own risk.
In order to access Prometheus from outside the GitLab server you will need to
set a FQDN or IP in `prometheus['listen_address']`.
To change the address/port that Prometheus listens on: To change the address/port that Prometheus listens on:
1. Edit `/etc/gitlab/gitlab.rb` 1. Edit `/etc/gitlab/gitlab.rb`
...@@ -80,9 +83,9 @@ You can visit `http://localhost:9090` for the dashboard that Prometheus offers b ...@@ -80,9 +83,9 @@ You can visit `http://localhost:9090` for the dashboard that Prometheus offers b
>**Note:** >**Note:**
If SSL has been enabled on your GitLab instance, you may not be able to access If SSL has been enabled on your GitLab instance, you may not be able to access
Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to Prometheus on the same browser as GitLab if using the same FQDN due to [HSTS][hsts]. We plan to
[provide access via GitLab][multi-user-prometheus], but in the interim there are [provide access via GitLab][multi-user-prometheus], but in the interim there are
some workarounds: using a separate browser for Prometheus, resetting HSTS, or some workarounds: using a separate FQDN, using server IP, using a separate browser for Prometheus, resetting HSTS, or
having [Nginx proxy it][nginx-custom-config]. having [Nginx proxy it][nginx-custom-config].
The performance data collected by Prometheus can be viewed directly in the The performance data collected by Prometheus can be viewed directly in the
......
...@@ -38,6 +38,7 @@ Example of response ...@@ -38,6 +38,7 @@ Example of response
"size": 1000 "size": 1000
}, },
"finished_at": "2015-12-24T17:54:27.895Z", "finished_at": "2015-12-24T17:54:27.895Z",
"artifacts_expire_at": "2016-01-23T17:54:27.895Z"
"id": 7, "id": 7,
"name": "teaspoon", "name": "teaspoon",
"pipeline": { "pipeline": {
...@@ -81,6 +82,7 @@ Example of response ...@@ -81,6 +82,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.727Z", "created_at": "2015-12-24T15:51:21.727Z",
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z", "finished_at": "2015-12-24T17:54:24.921Z",
"artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"id": 6, "id": 6,
"name": "rspec:other", "name": "rspec:other",
"pipeline": { "pipeline": {
...@@ -152,6 +154,7 @@ Example of response ...@@ -152,6 +154,7 @@ Example of response
"size": 1000 "size": 1000
}, },
"finished_at": "2015-12-24T17:54:27.895Z", "finished_at": "2015-12-24T17:54:27.895Z",
"artifacts_expire_at": "2016-01-23T17:54:27.895Z"
"id": 7, "id": 7,
"name": "teaspoon", "name": "teaspoon",
"pipeline": { "pipeline": {
...@@ -195,6 +198,7 @@ Example of response ...@@ -195,6 +198,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.727Z", "created_at": "2015-12-24T15:51:21.727Z",
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z", "finished_at": "2015-12-24T17:54:24.921Z",
"artifacts_expire_at": "2016-01-23T17:54:24.921Z"
"id": 6, "id": 6,
"name": "rspec:other", "name": "rspec:other",
"pipeline": { "pipeline": {
...@@ -261,6 +265,7 @@ Example of response ...@@ -261,6 +265,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.880Z", "created_at": "2015-12-24T15:51:21.880Z",
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z", "finished_at": "2015-12-24T17:54:31.198Z",
"artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"id": 8, "id": 8,
"name": "rubocop", "name": "rubocop",
"pipeline": { "pipeline": {
......
...@@ -133,7 +133,7 @@ PUT /application/settings ...@@ -133,7 +133,7 @@ PUT /application/settings
| `repository_checks_enabled` | boolean | no | GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. | | `repository_checks_enabled` | boolean | no | GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. |
| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. | | `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
| `require_two_factor_authentication` | boolean | no | Require all users to setup Two-factor authentication | | `require_two_factor_authentication` | boolean | no | Require all users to setup Two-factor authentication |
| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. | | `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. | | `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up | | `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up |
| `sentry_dsn` | string | yes (if `sentry_enabled` is true) | Sentry Data Source Name | | `sentry_dsn` | string | yes (if `sentry_enabled` is true) | Sentry Data Source Name |
......
# Auto Deploy This document was moved to [another location](../../topics/autodevops/index.md#auto-deploy).
> [Introduced][mr-8135] in GitLab 8.15.
> Auto deploy is an experimental feature and is **not recommended for Production use** at this time.
> As of GitLab 9.1, access to the container registry is only available while the
Pipeline is running. Restarting a pod, scaling a service, or other actions which
require on-going access **will fail**. On-going secure access is planned for a
subsequent release.
> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the
functionality has been included in [Auto
DevOps](../../topics/autodevops/index.md).
Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
templates for various infrastructure providers and deployment scripts
powering them. These scripts are responsible for packaging your application,
setting up the infrastructure and spinning up necessary services (for
example a database).
## How it works
The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy]
project which is used to simplify the deployment process to Kubernetes by
providing intelligent `build`, `deploy`, and `destroy` commands which you can
use in your `.gitlab-ci.yml` as is. It uses [Herokuish](https://github.com/gliderlabs/herokuish),
which uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
to do some of the work, plus some of GitLab's own tools to package it all up. For
your convenience, a [Docker image][kube-image] is also provided.
You can use the [Kubernetes project service](../../user/project/integrations/kubernetes.md)
to store credentials to your infrastructure provider and they will be available
during the deployment.
## Quick start
We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com.
For a demonstration of GitLab Auto Deploy, read the blog post [Auto Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/)
## Supported templates
The list of supported auto deploy templates is available in the
[gitlab-ci-yml project][auto-deploy-templates].
## Configuration
>**Note:**
In order to understand why the following steps are required, read the
[how it works](#how-it-works) section.
To configure Autodeploy, you will need to:
1. Enable a deployment [project service][project-services] to store your
credentials. For example, if you want to deploy to OpenShift you have to
enable [Kubernetes service][kubernetes-service].
1. Configure GitLab Runner to use the
[Docker or Kubernetes executor](https://docs.gitlab.com/runner/executors/) with
[privileged mode enabled][docker-in-docker].
1. Navigate to the "Project" tab and click "Set up auto deploy" button.
![Auto deploy button](img/auto_deploy_button.png)
1. Select a template.
![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png)
1. Commit your changes and create a merge request.
1. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you.
## Private project support
> Experimental support [introduced][mr-2] in GitLab 9.1.
When a project has been marked as private, GitLab's [Container Registry][container-registry] requires authentication when downloading containers. Auto deploy will automatically provide the required authentication information to Kubernetes, allowing temporary access to the registry. Authentication credentials will be valid while the pipeline is running, allowing for a successful initial deployment.
After the pipeline completes, Kubernetes will no longer be able to access the container registry. Restarting a pod, scaling a service, or other actions which require on-going access to the registry will fail. On-going secure access is planned for a subsequent release.
## PostgreSQL database support
> Experimental support [introduced][mr-8] in GitLab 9.1.
In order to support applications that require a database, [PostgreSQL][postgresql] is provisioned by default. Credentials to access the database are preconfigured, but can be customized by setting the associated [variables](#postgresql-variables). These credentials can be used for defining a `DATABASE_URL` of the format: `postgres://user:password@postgres-host:postgres-port/postgres-database`. It is important to note that the database itself is temporary, and contents will be not be saved.
PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`.
The following PostgreSQL variables are supported:
1. `DISABLE_POSTGRES: "yes"`: disable automatic deployment of PostgreSQL
1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL
1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL
1. `POSTGRES_DB: "my database"`: use custom database name for PostgreSQL
## Auto Monitoring
> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438).
Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for:
* Response Metrics: latency, throughput, error rate
* System Metrics: CPU utilization, memory utilization
Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md).
To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments).
![Auto Metrics](img/auto_monitoring.png)
### Configuring Auto Monitoring
If GitLab has been deployed using the [omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no configuration is required.
If you have installed GitLab using a different method:
1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2
[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8
[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
[project-services]: ../../user/project/integrations/project_services.md
[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/
...@@ -24,7 +24,7 @@ Environments are like tags for your CI jobs, describing where code gets deployed ...@@ -24,7 +24,7 @@ Environments are like tags for your CI jobs, describing where code gets deployed
Deployments are created when [jobs] deploy versions of code to environments, Deployments are created when [jobs] deploy versions of code to environments,
so every environment can have one or more deployments. GitLab keeps track of so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your your deployments, so you always know what is currently being deployed on your
servers. If you have a deployment service such as [Kubernetes][kubernetes-service] servers. If you have a deployment service such as [Kubernetes][kube]
enabled for your project, you can use it to assist with your deployments, and enabled for your project, you can use it to assist with your deployments, and
can even access a [web terminal](#web-terminals) for your environment from within GitLab! can even access a [web terminal](#web-terminals) for your environment from within GitLab!
...@@ -605,7 +605,7 @@ Web terminals were added in GitLab 8.15 and are only available to project ...@@ -605,7 +605,7 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners. masters and owners.
If you deploy to your environments with the help of a deployment service (e.g., If you deploy to your environments with the help of a deployment service (e.g.,
the [Kubernetes service][kubernetes-service]), GitLab can open the [Kubernetes integration][kube]), GitLab can open
a terminal session to your environment! This is a very powerful feature that a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service integration enable it, just follow the instructions given in the service integration
...@@ -671,7 +671,6 @@ Below are some links you may find interesting: ...@@ -671,7 +671,6 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md [Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs [jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md [yaml]: yaml/README.md
[kubernetes-service]: ../user/project/integrations/kubernetes.md
[environments]: #environments [environments]: #environments
[deployments]: #deployments [deployments]: #deployments
[permissions]: ../user/permissions.md [permissions]: ../user/permissions.md
...@@ -683,5 +682,5 @@ Below are some links you may find interesting: ...@@ -683,5 +682,5 @@ Below are some links you may find interesting:
[gitlab-flow]: ../workflow/gitlab_flow.md [gitlab-flow]: ../workflow/gitlab_flow.md
[gitlab runner]: https://docs.gitlab.com/runner/ [gitlab runner]: https://docs.gitlab.com/runner/
[git-strategy]: yaml/README.md#git-strategy [git-strategy]: yaml/README.md#git-strategy
[kube]: ../user/project/integrations/kubernetes.md [kube]: ../user/project/clusters/index.md
[prom]: ../user/project/integrations/prometheus.md [prom]: ../user/project/integrations/prometheus.md
...@@ -215,8 +215,8 @@ are set in the build environment. These variables are only defined for ...@@ -215,8 +215,8 @@ are set in the build environment. These variables are only defined for
[deployment jobs](../environments.md). Please consult the documentation of [deployment jobs](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define. the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is An example project service that defines deployment variables is the
[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables). [Kubernetes integration](../../user/project/clusters/index.md#deployment-variables).
## Debug tracing ## Debug tracing
......
# Icons # Icons and SVG Illustrations
We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are only loaded once and then referenced through an ID. The sprite SVG is located under `/assets/icons.svg`. Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome usages. We manage our own Icon and Illustration library in the [gitlab-svgs][gitlab-svgs] repository.
This repository is published on [npm][npm] and managed as a dependency via yarn.
You can browse all available Icons and Illustrations [here][svg-preview].
To upgrade to a new version run `yarn upgrade @gitlab-org/gitlab-svgs`.
### Usage in HAML/Rails ## Icons
To use a sprite Icon in HAML or Rails we use a specific helper function : We are using SVG Icons in GitLab with a SVG Sprite.
This means the icons are only loaded once, and are referenced through an ID.
The sprite SVG is located under `/assets/icons.svg`.
Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome icons.
`sprite_icon(icon_name, size: nil, css_class: '')` ### Usage in HAML/Rails
**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`). To use a sprite Icon in HAML or Rails we use a specific helper function :
**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class) ```ruby
sprite_icon(icon_name, size: nil, css_class: '')
```
**css_class (optional)** If you want to add additional css classes - **icon_name** Use the icon_name that you can find in the SVG Sprite
([Overview is available here][svg-preview]).
- **size (optional)** Use one of the following sizes : 16, 24, 32, 48, 72 (this will be translated into a `s16` class)
- **css_class (optional)** If you want to add additional css classes
**Example** **Example**
`= sprite_icon('issues', size: 72, css_class: 'icon-danger')` ```haml
= sprite_icon('issues', size: 72, css_class: 'icon-danger')
```
**Output from example above** **Output from example above**
`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>` ```html
<svg class="s72 icon-danger">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use>
</svg>
```
### Usage in Vue ### Usage in Vue
...@@ -28,33 +46,71 @@ We have a special Vue component for our sprite icons in `\vue_shared\components\ ...@@ -28,33 +46,71 @@ We have a special Vue component for our sprite icons in `\vue_shared\components\
Sample usage : Sample usage :
`<icon ```javascript
name="retry" <script>
:size="32" import Icon from "~/vue_shared/components/icon.vue"
css-classes="top"
/>` export default {
components: {
**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`). Icon,
},
**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes) };
<script>
**css-classes (optional)** Additional CSS Classes to add to the svg tag. <template>
<icon
name="issues"
:size="72"
css-classes="icon-danger"
/>
</template>
```
- **name** Name of the Icon in the SVG Sprite ([Overview is available here][svg-preview]).
- **size (optional)** Number value for the size which is then mapped to a specific CSS class
(Available Sizes: 8, 12, 16, 18, 24, 32, 48, 72 are mapped to `sXX` css classes)
- **css-classes (optional)** Additional CSS Classes to add to the svg tag.
### Usage in HTML/JS ### Usage in HTML/JS
Please use the following function inside JS to render an icon : Please use the following function inside JS to render an icon:
`gl.utils.spriteIcon(iconName)` `gl.utils.spriteIcon(iconName)`
## Adding a new icon to the sprite ## SVG Illustrations
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers.
Please use the class `svg-content` around it to ensure nice rendering.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`. ### Usage in HAML/Rails
# SVG Illustrations **Example**
Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. Please use the class `svg-content` around it to ensure nice rendering. The illustrations are also organised in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository (as they are then automatically optimised). ```haml
.svg-content
= image_tag 'illustrations/merge_requests.svg'
```
**Example** ### Usage in Vue
`= image_tag 'illustrations/merge_requests.svg'` To use an SVG illustrations in a template provide the path as a property and display it through a standard img tag.
Component:
```js
<script>
export default {
props: {
svgIllustrationPath: {
type: String,
required: true,
},
},
};
<script>
<template>
<img :src="svgIllustrationPath" />
</template>
```
[npm]: https://www.npmjs.com/package/@gitlab-org/gitlab-svgs
[gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs
[svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs
...@@ -54,8 +54,8 @@ Vuex specific design patterns and practices. ...@@ -54,8 +54,8 @@ Vuex specific design patterns and practices.
## [Axios](axios.md) ## [Axios](axios.md)
Axios specific practices and gotchas. Axios specific practices and gotchas.
## [Icons](icons.md) ## [Icons and Illustrations](icons.md)
How we use SVG for our Icons. How we use SVG for our Icons and Illustrations.
## [Components](components.md) ## [Components](components.md)
......
This diff is collapsed.
# Accessibility # Accessiblity
Using semantic HTML plays a key role when it comes to accessibility.
> TODO: Add content ## Accessible Rich Internet Applications - ARIA
WAI-ARIA, the Accessible Rich Internet Applications specification, defines a way to make Web content and Web applications more accessible to people with disabilities.
> Note: It is [recommended][using-aria] to use semantic elements as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
### Role
The `role` attribute describes the role the element plays in the context of the document.
Check the list of WAI-ARIA roles [here][roles]
## Icons
When using icons or images that aren't absolutely needed to understand the context, we should use `aria-hidden="true"`.
On the other hand, if an icon is crucial to understand the context we should do one of the following:
1. Use `aria-label` in the element with a meaningful description
1. Use `aria-labelledby` to point to an element that contains the explanation for that icon
## Form inputs
In forms we should use the `for` attribute in the label statement:
```
<div>
<label for="name">Fill in your name:</label>
<input type="text" id="name" name="name">
</div>
```
## Testing
1. On MacOS you can use [VoiceOver][voice-over] by pressing `cmd+F5`.
1. On Windows you can use [Narrator][narrator] by pressing Windows logo key + Ctrl + Enter.
## Online resources
- [Chrome Accessibility Developer Tools][dev-tools] for testing accessibility
- [Audit Rules Page][audit-rules] for best practices
- [Lighthouse Accessibility Score][lighthouse] for accessibility audits
[using-aria]: https://www.w3.org/TR/using-aria/#notes2
[dev-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
[aria-w3c]: https://www.w3.org/TR/wai-aria-1.1/
[roles]: https://www.w3.org/TR/wai-aria-1.1/#landmark_roles
[voice-over]: https://www.apple.com/accessibility/mac/vision/
[narrator]: https://www.microsoft.com/en-us/accessibility/windows
[lighthouse]: https://developers.google.com/web/tools/lighthouse/scoring#a11y
--- This document was moved to [another location](../clusters/index.md).
last_updated: 2017-12-28
---
# GitLab Kubernetes / OpenShift integration
CAUTION: **Warning:**
The Kubernetes service integration has been deprecated in GitLab 10.3. If the
service is active, the cluster information will still be editable, however we
advise to disable and reconfigure the clusters using the new
[Clusters](../clusters/index.md) page. If the service is inactive, the fields
will not be editable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
GitLab can be configured to interact with Kubernetes, or other systems using the
Kubernetes API (such as OpenShift).
Each project can be configured to connect to a different Kubernetes cluster, see
the [configuration](#configuration) section.
## Configuration
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
of your project and select the **Kubernetes** service to configure it. Fill in
all the needed parameters, check the "Active" checkbox and hit **Save changes**
for the changes to take effect.
![Kubernetes configuration settings](img/kubernetes_configuration.png)
The Kubernetes service takes the following parameters:
- **API URL** -
It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
exposes several APIs, we want the "base" URL that is common to all of them,
e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
- **CA certificate** (optional) -
If the API is using a self-signed TLS certificate, you'll also need to include
the `ca.crt` contents here.
- **Project namespace** (optional) - The following apply:
- By default you don't have to fill it in; by leaving it blank, GitLab will
create one for you.
- Each project should have a unique namespace.
- The project namespace is not necessarily the namespace of the secret, if
you're using a secret with broader permissions, like the secret from `default`.
- You should **not** use `default` as the project namespace.
- If you or someone created a secret specifically for the project, usually
with limited permissions, the secret's namespace and project namespace may
be the same.
- **Token** -
GitLab authenticates against Kubernetes using service tokens, which are
scoped to a particular `namespace`. If you don't have a service token yet,
you can follow the
[Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
to create one. You can also view or create service tokens in the
[Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
(under **Config > Secrets**).
TIP: **Tip:**
If you have a single cluster that you want to use for all your projects,
you can pre-fill the settings page with a default template. To configure the
template, see [Services Templates](services_templates.md).
## Deployment variables
The Kubernetes service exposes the following
[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
GitLab CI/CD build environment:
- `KUBE_URL` - Equal to the API URL.
- `KUBE_TOKEN` - The Kubernetes token.
- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
The default value is `<project_name>-<project_id>`. You can overwrite it to
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
receive the default value.
- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path
to a file containing PEM data.
- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
CA bundle would be embedded if specified.
## What you can get with the Kubernetes integration
Here's what you can do with GitLab if you enable the Kubernetes integration.
### Deploy Boards
> Available in [GitLab Premium][ee].
GitLab's Deploy Boards offer a consolidated view of the current health and
status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
displaying the status of the pods in the deployment. Developers and other
teammates can view the progress and status of a rollout, pod by pod, in the
workflow they already use without any need to access Kubernetes.
[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
### Canary Deployments
> Available in [GitLab Premium][ee].
Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
and visualize your canary deployments right inside the Deploy Board, without
the need to leave GitLab.
[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
### Kubernetes monitoring
Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
[NGINX ingress](./prometheus_library/nginx.md) is also supported.
[> Read more about Kubernetes monitoring](prometheus_library/kubernetes.md)
### Auto DevOps
Auto DevOps automatically detects, builds, tests, deploys, and monitors your
applications.
To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring)
you will need the Kubernetes project integration enabled.
[> Read more about Auto DevOps](../../../topics/autodevops/index.md)
### Web terminals
NOTE: **Note:**
Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
to use terminals. Support is limited to the first container in the
first pod of your environment.
When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
Docker and Kubernetes, so you get a new shell session within your existing
containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with
`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
[ee]: https://about.gitlab.com/products/
...@@ -39,7 +39,6 @@ Click on the service links to see further configuration instructions and details ...@@ -39,7 +39,6 @@ Click on the service links to see further configuration instructions and details
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker | | [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server | | JetBrains TeamCity CI | A continuous integration and build server |
| [Kubernetes](kubernetes.md) _(Has been deprecated in GitLab 10.3)_ | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | | [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors | | [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
......
...@@ -15,7 +15,8 @@ module API ...@@ -15,7 +15,8 @@ module API
include: [ include: [
GrapeLogging::Loggers::FilterParameters.new, GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new, GrapeLogging::Loggers::ClientEnv.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new
] ]
allow_access_with_scope :api allow_access_with_scope :api
......
...@@ -1020,6 +1020,7 @@ module API ...@@ -1020,6 +1020,7 @@ module API
class Job < JobBasic class Job < JobBasic
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :runner, with: Runner expose :runner, with: Runner
expose :artifacts_expire_at
end end
class JobBasicWithProject < JobBasic class JobBasicWithProject < JobBasic
......
...@@ -89,12 +89,6 @@ module API ...@@ -89,12 +89,6 @@ module API
end end
end end
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
def repository_path
repository.path_to_repo
end
# Return the Gitaly Address if it is enabled # Return the Gitaly Address if it is enabled
def gitaly_payload(action) def gitaly_payload(action)
return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action) return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action)
......
...@@ -59,7 +59,11 @@ module API ...@@ -59,7 +59,11 @@ module API
status: true, status: true,
gl_repository: gl_repository, gl_repository: gl_repository,
gl_username: user&.username, gl_username: user&.username,
repository_path: repository_path,
# This repository_path is a bogus value but gitlab-shell still requires
# its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135
repository_path: '/',
gitaly: gitaly_payload(params[:action]) gitaly: gitaly_payload(params[:action])
} }
end end
......
...@@ -123,6 +123,7 @@ module API ...@@ -123,6 +123,7 @@ module API
end end
put '/:id' do put '/:id' do
job = authenticate_job! job = authenticate_job!
forbidden!('Job is not running') unless job.running?
job.trace.set(params[:trace]) if params[:trace] job.trace.set(params[:trace]) if params[:trace]
...@@ -131,9 +132,9 @@ module API ...@@ -131,9 +132,9 @@ module API
case params[:state].to_s case params[:state].to_s
when 'success' when 'success'
job.success job.success!
when 'failed' when 'failed'
job.drop(params[:failure_reason] || :unknown_failure) job.drop!(params[:failure_reason] || :unknown_failure)
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment