Commit a091e24d authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-11-30

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 4b87f081 7afa3f47
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.2.3 (2017-11-30)
### Fixed (5 changes)
- Fix viewing default push rules on a Geo secondary. !3559
- Disable autocomplete for epics.
- Fix epic fullscreen editing.
- Fix tasklist for epics.
- Fix Geo wiki sync error not increasing retry count.
## 10.2.2 (2017-11-23) ## 10.2.2 (2017-11-23)
### Fixed (6 changes) ### Fixed (6 changes)
......
...@@ -2,6 +2,25 @@ ...@@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.2.3 (2017-11-30)
### Fixed (7 changes)
- Fix hashed storage for Import/Export uploads. !15482
- Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories. !15520
- Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories. !15600
- Fix WIP system note not being created.
- Fix link text from group context.
- Fix defaults for MR states and merge statuses.
- Fix pulling and pushing using a personal access token with the sudo scope.
### Performance (3 changes)
- Drastically improve project search performance by no longer searching namespace name.
- Reuse authors when rendering event Atom feeds.
- Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside.
## 10.2.2 (2017-11-23) ## 10.2.2 (2017-11-23)
### Fixed (5 changes) ### Fixed (5 changes)
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
noteType: constants.COMMENT, noteType: constants.COMMENT,
// Can't use mapGetters, // Can't use mapGetters,
// this needs to be in the data object because it belongs to the state // this needs to be in the data object because it belongs to the state
issueState: this.$store.getters.getIssueData.state, issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false, isSubmitting: false,
isSubmitButtonDisabled: true, isSubmitButtonDisabled: true,
}; };
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
...mapGetters([ ...mapGetters([
'getCurrentUserLastNote', 'getCurrentUserLastNote',
'getUserData', 'getUserData',
'getIssueData', 'getNoteableData',
'getNotesData', 'getNotesData',
]), ]),
isLoggedIn() { isLoggedIn() {
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
}, },
canCreateNote() { canCreateNote() {
return this.getIssueData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
issueActionButtonTitle() { issueActionButtonTitle() {
if (this.note.length) { if (this.note.length) {
...@@ -85,16 +85,16 @@ ...@@ -85,16 +85,16 @@
return this.getNotesData.quickActionsDocsPath; return this.getNotesData.quickActionsDocsPath;
}, },
markdownPreviewPath() { markdownPreviewPath() {
return this.getIssueData.preview_note_path; return this.getNoteableData.preview_note_path;
}, },
author() { author() {
return this.getUserData; return this.getUserData;
}, },
canUpdateIssue() { canUpdateIssue() {
return this.getIssueData.current_user.can_update; return this.getNoteableData.current_user.can_update;
}, },
endpoint() { endpoint() {
return this.getIssueData.create_note_path; return this.getNoteableData.create_note_path;
}, },
}, },
methods: { methods: {
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
data: { data: {
note: { note: {
noteable_type: constants.NOTEABLE_TYPE, noteable_type: constants.NOTEABLE_TYPE,
noteable_id: this.getIssueData.id, noteable_id: this.getNoteableData.id,
note: this.note, note: this.note,
}, },
}, },
...@@ -207,7 +207,7 @@ ...@@ -207,7 +207,7 @@
}, },
initAutoSave() { initAutoSave() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue'); this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue');
} }
}, },
initTaskList() { initTaskList() {
...@@ -266,9 +266,9 @@ ...@@ -266,9 +266,9 @@
<div class="error-alert"></div> <div class="error-alert"></div>
<issue-warning <issue-warning
v-if="hasWarning(getIssueData)" v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getIssueData)" :is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getIssueData)" :is-confidential="isConfidential(getNoteableData)"
/> />
<markdown-field <markdown-field
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
], ],
computed: { computed: {
...mapGetters([ ...mapGetters([
'getIssueData', 'getNoteableData',
]), ]),
discussion() { discussion() {
return this.note.notes[0]; return this.note.notes[0];
...@@ -50,10 +50,10 @@ ...@@ -50,10 +50,10 @@
return this.discussion.author; return this.discussion.author;
}, },
canReply() { canReply() {
return this.getIssueData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
newNotePath() { newNotePath() {
return this.getIssueData.create_note_path; return this.getNoteableData.create_note_path;
}, },
lastUpdatedBy() { lastUpdatedBy() {
const { notes } = this.note; const { notes } = this.note;
......
...@@ -46,8 +46,8 @@ ...@@ -46,8 +46,8 @@
computed: { computed: {
...mapGetters([ ...mapGetters([
'getDiscussionLastNote', 'getDiscussionLastNote',
'getIssueData', 'getNoteableData',
'getIssueDataByProp', 'getNoteableDataByProp',
'getNotesDataByProp', 'getNotesDataByProp',
'getUserDataByProp', 'getUserDataByProp',
]), ]),
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
return `#note_${this.noteId}`; return `#note_${this.noteId}`;
}, },
markdownPreviewPath() { markdownPreviewPath() {
return this.getIssueDataByProp('preview_note_path'); return this.getNoteableDataByProp('preview_note_path');
}, },
markdownDocsPath() { markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath'); return this.getNotesDataByProp('markdownDocsPath');
...@@ -129,9 +129,9 @@ ...@@ -129,9 +129,9 @@
class="edit-note common-note-form js-quick-submit gfm-form"> class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning <issue-warning
v-if="hasWarning(getIssueData)" v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getIssueData)" :is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getIssueData)" :is-confidential="isConfidential(getNoteableData)"
/> />
<markdown-field <markdown-field
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
export default { export default {
name: 'issueNotesApp', name: 'issueNotesApp',
props: { props: {
issueData: { noteableData: {
type: Object, type: Object,
required: true, required: true,
}, },
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
actionToggleAward: 'toggleAward', actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
setNotesData: 'setNotesData', setNotesData: 'setNotesData',
setIssueData: 'setIssueData', setNoteableData: 'setNoteableData',
setUserData: 'setUserData', setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt', setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash', setTargetNoteHash: 'setTargetNoteHash',
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
}, },
created() { created() {
this.setNotesData(this.notesData); this.setNotesData(this.notesData);
this.setIssueData(this.issueData); this.setNoteableData(this.noteableData);
this.setUserData(this.userData); this.setUserData(this.userData);
}, },
mounted() { mounted() {
......
...@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
return { return {
issueData: JSON.parse(notesDataset.issueData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: { notesData: {
lastFetchedAt: notesDataset.lastFetchedAt, lastFetchedAt: notesDataset.lastFetchedAt,
...@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
render(createElement) { render(createElement) {
return createElement('issue-notes-app', { return createElement('issue-notes-app', {
props: { props: {
issueData: this.issueData, noteableData: this.noteableData,
notesData: this.notesData, notesData: this.notesData,
userData: this.currentUserData, userData: this.currentUserData,
}, },
......
...@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data); export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
......
...@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash; ...@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData; export const getNotesData = state => state.notesData;
export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getIssueData = state => state.issueData; export const getNoteableData = state => state.noteableData;
export const getIssueDataByProp = state => prop => state.issueData[prop]; export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
......
...@@ -15,7 +15,7 @@ export default new Vuex.Store({ ...@@ -15,7 +15,7 @@ export default new Vuex.Store({
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: {}, notesData: {},
userData: {}, userData: {},
issueData: {}, noteableData: {},
}, },
actions, actions,
getters, getters,
......
...@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; ...@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE'; export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA'; export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_ISSUE_DATA = 'SET_ISSUE_DATA'; export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA'; export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES'; export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
......
...@@ -66,8 +66,8 @@ export default { ...@@ -66,8 +66,8 @@ export default {
Object.assign(state, { notesData: data }); Object.assign(state, { notesData: data });
}, },
[types.SET_ISSUE_DATA](state, data) { [types.SET_NOTEABLE_DATA](state, data) {
Object.assign(state, { issueData: data }); Object.assign(state, { noteableData: data });
}, },
[types.SET_USER_DATA](state, data) { [types.SET_USER_DATA](state, data) {
......
...@@ -5,6 +5,11 @@ module Clusters ...@@ -5,6 +5,11 @@ module Clusters
include Gitlab::Kubernetes include Gitlab::Kubernetes
include ReactiveCaching include ReactiveCaching
<<<<<<< HEAD
=======
prepend EE::KubernetesService
>>>>>>> origin/master
self.table_name = 'cluster_platforms_kubernetes' self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] } self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
...@@ -85,6 +90,19 @@ module Clusters ...@@ -85,6 +90,19 @@ module Clusters
# network access # network access
def calculate_reactive_cache def calculate_reactive_cache
return unless enabled? && project && !project.pending_delete? return unless enabled? && project && !project.pending_delete?
<<<<<<< HEAD
# We may want to cache extra things in the future
{ pods: read_pods }
end
def kubeclient
@kubeclient ||= build_kubeclient!
end
private
=======
# We may want to cache extra things in the future # We may want to cache extra things in the future
{ pods: read_pods } { pods: read_pods }
...@@ -96,12 +114,83 @@ module Clusters ...@@ -96,12 +114,83 @@ module Clusters
private private
>>>>>>> origin/master
def kubeconfig def kubeconfig
to_kubeconfig( to_kubeconfig(
url: api_url, url: api_url,
namespace: actual_namespace, namespace: actual_namespace,
token: token, token: token,
ca_pem: ca_pem) ca_pem: ca_pem)
<<<<<<< HEAD
end
def default_namespace
return unless project
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
end
::Kubeclient::Client.new(
join_api_url(api_path),
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
)
end
# Returns a hash of all pods in the namespace
def read_pods
kubeclient = build_kubeclient!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
[]
end
def kubeclient_ssl_options
opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
if ca_pem.present?
opts[:cert_store] = OpenSSL::X509::Store.new
opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
end
opts
end
def kubeclient_auth_options
{ bearer_token: token }
end
def join_api_url(api_path)
url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '')
url.path = [prefix, api_path].join("/")
url.to_s
end
def terminal_auth
{
token: token,
ca_pem: ca_pem,
max_session_time: current_application_settings.terminal_max_session_time
}
end
=======
end end
def default_namespace def default_namespace
...@@ -170,6 +259,7 @@ module Clusters ...@@ -170,6 +259,7 @@ module Clusters
} }
end end
>>>>>>> origin/master
def enforce_namespace_to_lower_case def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase self.namespace = self.namespace&.downcase
end end
......
...@@ -146,7 +146,7 @@ class Environment < ActiveRecord::Base ...@@ -146,7 +146,7 @@ class Environment < ActiveRecord::Base
end end
def rollout_status def rollout_status
project.deployment_platform.rollout_status(self) if deployment_service_ready? project.deployment_service.rollout_status(self) if deployment_service_ready?
end end
def has_metrics? def has_metrics?
......
...@@ -23,7 +23,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -23,7 +23,7 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment) stop_project_environment_path(environment.project, environment)
end end
expose :terminal_path, if: ->(*) { environment.deployment_service_ready? } do |environment| expose :terminal_path, if: ->(*) { environment.has_terminals? } do |environment|
can?(request.current_user, :admin_environment, environment.project) && can?(request.current_user, :admin_environment, environment.project) &&
terminal_project_environment_path(environment.project, environment) terminal_project_environment_path(environment.project, environment)
end end
......
- if environment.deployment_service_ready? && can?(current_user, :admin_environment, @project) - if environment.has_terminals? && can?(current_user, :admin_environment, @project)
= link_to terminal_project_environment_path(@project, environment), class: 'btn terminal-button' do = link_to terminal_project_environment_path(@project, environment), class: 'btn terminal-button' do
= icon('terminal') = icon('terminal')
...@@ -13,5 +13,5 @@ ...@@ -13,5 +13,5 @@
quick_actions_docs_path: help_page_path('user/project/quick_actions'), quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url, notes_path: notes_url,
last_fetched_at: Time.now.to_i, last_fetched_at: Time.now.to_i,
issue_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json } } current_user_data: UserSerializer.new.represent(current_user).to_json } }
...@@ -7,7 +7,7 @@ module Geo ...@@ -7,7 +7,7 @@ module Geo
end end
def schedule_job(object_db_id, object_type) def schedule_job(object_db_id, object_type)
job_id = GeoFileDownloadWorker.perform_async(object_type, object_db_id) job_id = FileDownloadWorker.perform_async(object_type, object_db_id)
{ id: object_db_id, type: object_type, job_id: job_id } if job_id { id: object_db_id, type: object_type, job_id: job_id } if job_id
end end
......
module Geo
class FileDownloadWorker
include Sidekiq::Worker
sidekiq_options queue: :geo_file_download, retry: 3, dead: false
def perform(object_type, object_id)
Geo::FileDownloadService.new(object_type.to_sym, object_id).execute
end
end
end
...@@ -2,7 +2,7 @@ module Geo ...@@ -2,7 +2,7 @@ module Geo
class ProjectSyncWorker class ProjectSyncWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: :geo, retry: 3, dead: false sidekiq_options queue: :geo_project_sync, retry: 3, dead: false
sidekiq_retry_in { |count| 30 * count } sidekiq_retry_in { |count| 30 * count }
......
class GeoFileDownloadWorker
include Sidekiq::Worker
include GeoQueue
def perform(object_type, object_id)
Geo::FileDownloadService.new(object_type.to_sym, object_id).execute
end
end
---
title: Fix viewing default push rules on a Geo secondary
merge_request: 3559
author:
type: fixed
---
title: Disable autocomplete for epics
merge_request:
author:
type: fixed
---
title: Fix epic fullscreen editing
merge_request:
author:
type: fixed
---
title: Fix tasklist for epics
merge_request:
author:
type: fixed
---
title: Fix Geo wiki sync error not increasing retry count
merge_request:
author:
type: fixed
---
title: Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories
merge_request: 15520
author:
type: fixed
---
title: Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories
merge_request: 15600
author:
type: fixed
---
title: Fix defaults for MR states and merge statuses
merge_request:
author:
type: fixed
---
title: Fix pulling and pushing using a personal access token with the sudo scope
merge_request:
author:
type: fixed
---
title: Drastically improve project search performance by no longer searching namespace
name
merge_request:
author:
type: performance
---
title: Reuse authors when rendering event Atom feeds
merge_request:
author:
type: performance
---
title: Fix hashed storage for Import/Export uploads
merge_request: 15482
author:
type: fixed
---
title: Fix WIP system note not being created
merge_request:
author:
type: fixed
---
title: Fix link text from group context
merge_request:
author:
type: fixed
---
title: Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside
merge_request:
author:
type: performance
...@@ -75,7 +75,8 @@ ...@@ -75,7 +75,8 @@
- [repository_update_remote_mirror, 1] - [repository_update_remote_mirror, 1]
- [project_update_repository_storage, 1] - [project_update_repository_storage, 1]
- [admin_emails, 1] - [admin_emails, 1]
- [geo_repository_update, 1] - [geo_project_sync, 1]
- [geo_file_download, 1]
- [elastic_batch_project_indexer, 1] - [elastic_batch_project_indexer, 1]
- [elastic_indexer, 1] - [elastic_indexer, 1]
- [elastic_commit_indexer, 1] - [elastic_commit_indexer, 1]
......
...@@ -43,9 +43,13 @@ AuthorizedKeysCommand /opt/gitlab-shell/authorized_keys %u %k ...@@ -43,9 +43,13 @@ AuthorizedKeysCommand /opt/gitlab-shell/authorized_keys %u %k
AuthorizedKeysCommandUser git AuthorizedKeysCommandUser git
``` ```
Reload the sshd service: Reload OpenSSH:
``` ```bash
# Debian or Ubuntu installations
sudo service ssh reload
# CentOS installations
sudo service sshd reload sudo service sshd reload
``` ```
......
...@@ -4,50 +4,63 @@ ...@@ -4,50 +4,63 @@
## Legacy Storage ## Legacy Storage
Legacy Storage is the storage behavior prior to version 10.0. For historical reasons, GitLab replicated the same Legacy Storage is the storage behavior prior to version 10.0. For historical
mapping structure from the projects URLs: reasons, GitLab replicated the same mapping structure from the projects URLs:
* Project's repository: `#{namespace}/#{project_name}.git` * Project's repository: `#{namespace}/#{project_name}.git`
* Project's wiki: `#{namespace}/#{project_name}.wiki.git` * Project's wiki: `#{namespace}/#{project_name}.wiki.git`
This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the This structure made it simple to migrate from existing solutions to GitLab and
repository is stored. easy for Administrators to find where the repository is stored.
On the other hand this has some drawbacks: On the other hand this has some drawbacks:
Storage location will concentrate huge amount of top-level namespaces. The impact can be reduced by the introduction of [multiple storage paths][storage-paths]. Storage location will concentrate huge amount of top-level namespaces. The
impact can be reduced by the introduction of [multiple storage
paths][storage-paths].
Because Backups are a snapshot of the same URL mapping, if you try to recover a very old backup, you need to verify Because backups are a snapshot of the same URL mapping, if you try to recover a
if any project has taken the place of an old removed project sharing the same URL. This means that `mygroup/myproject` very old backup, you need to verify whether any project has taken the place of
from your backup may not be the same original project that is today in the same URL. an old removed or renamed project sharing the same URL. This means that
`mygroup/myproject` from your backup may not be the same original project that
is at that same URL today.
Any change in the URL will need to be reflected on disk (when groups / users or projects are renamed). This can add a lot Any change in the URL will need to be reflected on disk (when groups / users or
of load in big installations, and can be even worst if they are using any type of network based filesystem. projects are renamed). This can add a lot of load in big installations,
especially if using any type of network based filesystem.
Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct For GitLab Geo in particular: Geo does work with legacy storage, but in some
order or we may end-up with wrong repository or missing data temporarily. edge cases due to race conditions it can lead to errors when a project is
renamed multiple times in short succession, or a project is deleted and
recreated under the same name very quickly. We expect these race events to be
rare, and we have not observed a race condition side-effect happening yet.
This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts, This pattern also exists in other objects stored in GitLab, like issue
Docker Containers for the integrated Registry, etc. Attachments, GitLab Pages artifacts, Docker Containers for the integrated
Registry, etc.
## Hashed Storage ## Hashed Storage
Hashed Storage is the new storage behavior we are rolling out with 10.0. It's not enabled by default yet, but we > **Warning:** Hashed storage is in **Alpha**. For the latest updates, check the
encourage everyone to try-it and take the time to fix any script you may have that depends on the old behavior. > associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821)
> and please report any problems you encounter.
Instead of coupling project URL and the folder structure where the repository will be stored on disk, we are coupling Hashed Storage is the new storage behavior we are rolling out with 10.0. Instead
a hash, based on the project's ID. of coupling project URL and the folder structure where the repository will be
stored on disk, we are coupling a hash, based on the project's ID. This makes
the folder structure immutable, and therefore eliminates any requirement to
synchronize state from URLs to disk structure. This means that renaming a group,
user, or project will cost only the database transaction, and will take effect
immediately.
This makes the folder structure immutable, and therefore eliminates any requirement to synchronize state from URLs to The hash also helps to spread the repositories more evenly on the disk, so the
disk structure. This means that renaming a group, user or project will cost only the database transaction, and will take top-level directory will contain less folders than the total amount of top-level
effect immediately. namespaces.
The hash also helps to spread the repositories more evenly on the disk, so the top-level directory will contain less The hash format is based on the hexadecimal representation of SHA256:
folders than the total amount of top-level namespaces. `SHA256(project.id)`. The top-level folder uses the first 2 characters, followed
by another folder with the next 2 characters. They are both stored in a special
Hash format is based on hexadecimal representation of SHA256: `SHA256(project.id)`. `@hashed` folder, to be able to co-exist with existing Legacy Storage projects:
Top-level folder uses first 2 characters, followed by another folder with the next 2 characters. They are both stored in
a special folder `@hashed`, to co-exist with existing Legacy projects:
```ruby ```ruby
# Project's repository: # Project's repository:
...@@ -57,15 +70,13 @@ a special folder `@hashed`, to co-exist with existing Legacy projects: ...@@ -57,15 +70,13 @@ a special folder `@hashed`, to co-exist with existing Legacy projects:
"@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.wiki.git" "@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.wiki.git"
``` ```
This new format also makes possible to restore backups with confidence, as when restoring a repository from the backup,
you will never mistakenly restore a repository in the wrong project (considering the backup is made after the migration).
### How to migrate to Hashed Storage ### How to migrate to Hashed Storage
In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select In GitLab, go to **Admin > Settings**, find the **Repository Storage** section
"_Create new projects using hashed storage paths_". and select "_Create new projects using hashed storage paths_".
To migrate your existing projects to the new storage type, check the specific [rake tasks]. To migrate your existing projects to the new storage type, check the specific
[rake tasks].
[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283 [ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage [rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
...@@ -73,11 +84,13 @@ To migrate your existing projects to the new storage type, check the specific [r ...@@ -73,11 +84,13 @@ To migrate your existing projects to the new storage type, check the specific [r
### Hashed Storage coverage ### Hashed Storage coverage
We are incrementally moving every storable object in GitLab to the Hashed Storage pattern. You can check the current We are incrementally moving every storable object in GitLab to the Hashed
coverage status below. Storage pattern. You can check the current coverage status below (and also see
the [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821)).
Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not Note that things stored in an S3 compatible endpoint will not have the downsides
prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects. mentioned earlier, if they are not prefixed with `#{namespace}/#{project_name}`,
which is true for CI Cache and LFS Objects.
| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version | | Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version |
| --------------- | -------------- | -------------- | ------------- | -------------- | | --------------- | -------------- | -------------- | ------------- | -------------- |
......
...@@ -87,16 +87,12 @@ Meanwhile, the primary node will start to notify changes to the secondary, which ...@@ -87,16 +87,12 @@ Meanwhile, the primary node will start to notify changes to the secondary, which
will act on those notifications immediately. Make sure the secondary instance is will act on those notifications immediately. Make sure the secondary instance is
running and accessible. running and accessible.
### Step 2. Enabling hashed storage (from GitLab 10.0) ### Step 2. Enabling hashed storage (optional, from GitLab 10.0)
>**Warning** >**Warning**
Hashed storage is in **Beta**. It is considered experimental and not Hashed storage is in **Alpha**. It is considered experimental and not
production-ready. For the latest updates, check production-ready. See [Hashed
[issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821). Storage](../administration/repository_storage_types.md) for more detail.
Hashed Storage is not required to run GitLab Geo, but in some edge cases race
conditions can lead to errors and Geo to break. Known issues are renaming a
project multiple times in short succession, deleting a project and recreating
with the same name very quickly.
Using hashed storage significantly improves Geo replication - project and group Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes. renames no longer require synchronization between nodes.
......
...@@ -91,21 +91,12 @@ primary in a process known as backfill. Meanwhile, the primary node will start ...@@ -91,21 +91,12 @@ primary in a process known as backfill. Meanwhile, the primary node will start
to notify changes to the secondary, which will act on those notifications to notify changes to the secondary, which will act on those notifications
immediately. Make sure the secondary instance is running and accessible. immediately. Make sure the secondary instance is running and accessible.
### Step 2. Enabling hashed storage (from GitLab 10.0) ### Step 2. Enabling hashed storage (optional, GitLab 10.0)
>**Note:** >**Warning**
Hashed storage is in **Beta**. It is considered experimental and not Hashed storage is in **Alpha**. It is considered experimental and not
production-ready. For the latest updates, check production-ready. See [Hashed
[issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821). Storage](../administration/repository_storage_types.md) for more detail.
Hashed Storage is not required to run GitLab Geo, but in some edge cases race
conditions can lead to errors and Geo to break. Known issues are renaming a
project multiple times in short succession, deleting a project and recreating
with the same name very quickly.
>**Note:**
Instances already using hashed storage are not recommended to disable hashed
storage, since bugs affecting hashed storage would continue to affect these
projects.
Using hashed storage significantly improves Geo replication - project and group Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes. renames no longer require synchronization between nodes.
......
...@@ -26,7 +26,7 @@ connect to secondary database servers (which are read-only too). ...@@ -26,7 +26,7 @@ connect to secondary database servers (which are read-only too).
In many databases documentation you will see "primary" being referenced as "master" In many databases documentation you will see "primary" being referenced as "master"
and "secondary" as either "slave" or "standby" server (read-only). and "secondary" as either "slave" or "standby" server (read-only).
Since GitLab 9.4: We recommend using [PostgreSQL replication We recommend using [PostgreSQL replication
slots](https://medium.com/@tk512/replication-slots-in-postgresql-b4b03d277c75) slots](https://medium.com/@tk512/replication-slots-in-postgresql-b4b03d277c75)
to ensure the primary retains all the data necessary for the secondaries to to ensure the primary retains all the data necessary for the secondaries to
recover. See below for more details. recover. See below for more details.
...@@ -80,6 +80,7 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -80,6 +80,7 @@ will not be able to perform all necessary configuration steps. Refer to
else. else.
1. Set up TLS support for the PostgreSQL primary server 1. Set up TLS support for the PostgreSQL primary server
> **Warning**: Only skip this step if you **know** that PostgreSQL traffic > **Warning**: Only skip this step if you **know** that PostgreSQL traffic
> between the primary and secondary will be secured through some other > between the primary and secondary will be secured through some other
> means, e.g., a known-safe physical network path or a site-to-site VPN that > means, e.g., a known-safe physical network path or a site-to-site VPN that
...@@ -146,104 +147,115 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -146,104 +147,115 @@ will not be able to perform all necessary configuration steps. Refer to
postgresql['ssl'] = 'on' postgresql['ssl'] = 'on'
``` ```
1. Configure PostgreSQL to listen on an external network interface 1. Configure PostgreSQL to listen on network interfaces
Edit `/etc/gitlab/gitlab.rb` and add the following. Note that GitLab 9.1 added For security reasons, PostgreSQL does not listen on any network interfaces
the `geo_primary_role` configuration variable: by default. However, GitLab Geo requires the secondary to be able to
connect to the primary's database. For this reason, we need the address of
each node.
```ruby If you are using a cloud provider, you can lookup the addresses for each
geo_primary_role['enable'] = true Geo node through their management console. A table of terminology is
postgresql['listen_address'] = '1.2.3.4' provided below because terminology varies between vendors.
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32']
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32']
# New for 9.4: Set this to be the number of Geo secondary nodes you have
postgresql['max_replication_slots'] = 1
# postgresql['max_wal_senders'] = 10
# postgresql['wal_keep_segments'] = 10
```
For external PostgreSQL instances, [see additional instructions][external postgresql]. | GitLab Terminology | Amazon Web Services | Google Cloud Platform |
|-----|-----|-----|-----|
| Interface address | Private address | Internal address |
| Public address | Public address | External address |
To lookup the address of a Geo node, on the Geo node execute:
Where `1.2.3.4` is the IP address of the primary server, and `5.6.7.8` ```bash
is the IP address of the secondary one. # Interface address
ip route get 255.255.255.255 | awk '{print $NF; exit}'
For security reasons, PostgreSQL by default only listens on the local # Public address
interface (e.g. 127.0.0.1). However, GitLab Geo needs to communicate curl ipinfo.io/ip
between the primary and secondary nodes over a common network, such as a ```
corporate LAN or the public Internet. For this reason, we need to
configure PostgreSQL to listen on more interfaces.
The `listen_address` option opens PostgreSQL up to external connections In most cases, the following addresses will be used to configure GitLab
with the interface corresponding to the given IP. See [the PostgreSQL Geo:
documentation](https://www.postgresql.org/docs/9.6/static/runtime-config-connection.html)
for more details.
Note that if you are running GitLab Geo with a cloud provider (e.g. Amazon | Configuration | Address |
Web Services), the internal interface IP (as provided by `ifconfig`) may |-----|-----|
be different from the public IP address. For example, suppose you have a | `postgresql['listen_address']` | Primary's interface address |
nodes with the following configuration: | `postgresql['trust_auth_cidr_addresses']` | Primary's interface address |
| `postgresql['md5_auth_cidr_addresses']` | Secondary's public addresses |
|Node Type|Internal IP|External IP| The `listen_address` option opens PostgreSQL up to network connections
|---------|-----------|-----------| with the interface corresponding to the given address. See [the PostgreSQL
|Primary|10.1.5.3|54.193.124.100| documentation](https://www.postgresql.org/docs/9.6/static/runtime-config-connection.html)
|Secondary|10.1.10.5|54.193.100.155| for more details.
If you are running two nodes in different cloud availability zones, you Depending on your network configuration, the suggested addresses may not
may need to double check that the nodes can communicate over the internal be correct. If your primary and secondary connect over a local
IP addresses. For example, servers on Amazon Web Services in the same area network, or a virtual network connecting availability zones like
[Virtual Private Cloud (VPC)](https://aws.amazon.com/vpc/) can do Amazon's [VPC](https://aws.amazon.com/vpc/) of Google's [VPC](https://cloud.google.com/vpc/)
this. Google Compute Engine also offers an [internal network] you should use the secondary's interface address for `postgresql['md5_auth_cidr_addresses']`.
(https://cloud.google.com/compute/docs/networking) that supports
cross-availability zone networking.
For the above example, the following configuration uses the internal IPs Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
to replicate the database from the primary to the secondary: addresses with addresses appropriate to your network configuration:
```ruby ```ruby
# Example configuration using internal IPs for a cloud configuration
geo_primary_role['enable'] = true geo_primary_role['enable'] = true
postgresql['listen_address'] = '10.1.5.3'
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','10.1.5.3/32'] # Primary address
postgresql['md5_auth_cidr_addresses'] = ['10.1.10.5/32'] # - replace '1.2.3.4' with the primary interface address
postgresql['max_replication_slots'] = 1 # Number of Geo secondary nodes postgresql['listen_address'] = '1.2.3.4'
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32']
# Secondary addresses
# - replace '5.6.7.8' with the secondary public address
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32']
# Replication settings
# - set this to be the number of Geo secondary nodes you have
postgresql['max_replication_slots'] = 1
# postgresql['max_wal_senders'] = 10 # postgresql['max_wal_senders'] = 10
# postgresql['wal_keep_segments'] = 10 # postgresql['wal_keep_segments'] = 10
# Disable automatic database migrations for now
# (until PostgreSQL is restarted and listening on the interface address)
gitlab_rails['auto_migrate'] = false
``` ```
If you prefer that your nodes communicate over the public Internet, you For external PostgreSQL instances, [see additional instructions][external postgresql].
may choose the IP addresses from the "External IP" column above.
1. Optional: If you want to add another secondary, the relevant setting would look like: 1. Optional: If you want to add another secondary, the relevant setting would look like:
```ruby ```ruby
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32','11.22.33.44/32'] postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32','9.10.11.12/32']
``` ```
You may also want to edit the `wal_keep_segments` and `max_wal_senders` to You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
match your database replication requirements. Consult the [PostgreSQL - Replication documentation](https://www.postgresql.org/docs/9.6/static/runtime-config-replication.html) match your database replication requirements. Consult the [PostgreSQL -
Replication documentation](https://www.postgresql.org/docs/9.6/static/runtime-config-replication.html)
for more information. for more information.
1. Save the file and [reconfigure GitLab][] for the database listen changes to 1. Save the file and [reconfigure GitLab][] for the database listen changes and
take effect. the replication slot changes to be applied.
**This step will fail.** This is caused by
[Omnibus#2797](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2797).
Restart PostgreSQL: Restart PostgreSQL for its changes to take effect:
```bash ```bash
gitlab-ctl restart postgresql gitlab-ctl restart postgresql
``` ```
[Reconfigure GitLab][reconfigure GitLab] again. It should complete cleanly. 1. Reenable migrations
Edit `/etc/gitlab/gitlab.rb` and **delete** the following lines:
```ruby
# Disable automatic database migrations for now
# (until PostgreSQL is restarted and listening on the interface address)
gitlab_rails['auto_migrate'] = false
```
1. New for 9.4: Restart your primary PostgreSQL server to ensure the Save the file and [reconfigure GitLab][].
replication slot changes take effect (`sudo gitlab-ctl restart postgresql`
for Omnibus-provided PostgreSQL).
1. Now that the PostgreSQL server is set up to accept remote connections, run 1. Now that the PostgreSQL server is set up to accept remote connections, run
`netstat -plnt` to make sure that PostgreSQL is listening on port `5432` to `netstat -plnt` to make sure that PostgreSQL is listening on port `5432` to
the server's public IP. the server's interface address.
1. Verify that clock synchronization is enabled. 1. Verify that clock synchronization is enabled.
......
...@@ -53,18 +53,29 @@ secondary if ever promoted to a primary: ...@@ -53,18 +53,29 @@ secondary if ever promoted to a primary:
sudo -u git -H rm ~git/.ssh/id_rsa ~git/.ssh/id_rsa.pub sudo -u git -H rm ~git/.ssh/id_rsa ~git/.ssh/id_rsa.pub
``` ```
### Hashed Storage
>**Warning**
Hashed storage is in **Alpha**. It is considered experimental and not
production-ready. See [Hashed
Storage](../administration/repository_storage_types.md) for more detail.
If you previously enabled Hashed Storage and migrated all your existing
projects to Hashed Storage, disabling hashed storage will not migrate projects
to their previous project based storage path. As such, once enabled and
migrated we recommend leaving Hashed Storage enabled.
## Upgrading to GitLab 10.1 ## Upgrading to GitLab 10.1
>**Warning**
Hashed storage is in **Alpha**. It is considered experimental and not
production-ready. See [Hashed
Storage](../administration/repository_storage_types.md) for more detail.
[Hashed storage](../administration/repository_storage_types.md) was introduced [Hashed storage](../administration/repository_storage_types.md) was introduced
in GitLab 10.0, and a [migration path](../administration/raketasks/storage.md) in GitLab 10.0, and a [migration path](../administration/raketasks/storage.md)
for existing repositories was added in GitLab 10.1. for existing repositories was added in GitLab 10.1.
After upgrading to GitLab 10.1, we recommend that you
[enable hashed storage for all new projects](#step-5-enabling-hashed-storage-from-gitlab-100),
then [migrate existing projects to hashed storage](../administration/raketasks/storage.md).
This will significantly reduce the amount of synchronization required between
nodes in the event of project or group renames.
## Upgrading to GitLab 10.0 ## Upgrading to GitLab 10.0
Since GitLab 10.0, we require all **Geo** systems to [use SSH key lookups via Since GitLab 10.0, we require all **Geo** systems to [use SSH key lookups via
......
...@@ -22,7 +22,7 @@ Among numerous use cases for exporting issues for CSV, we can name a few: ...@@ -22,7 +22,7 @@ Among numerous use cases for exporting issues for CSV, we can name a few:
- Make a snapshot of issues for offline analysis or to communicate with other teams who may not be in GitLab - Make a snapshot of issues for offline analysis or to communicate with other teams who may not be in GitLab
- Create diagrams, graphs, and charts from the CSV data - Create diagrams, graphs, and charts from the CSV data
- Present the data in any other format for auditing or sharing reasons - Present the data in any other format for auditing or sharing reasons
- Import the issues elsewhere - Import the issues elsewhere to a system outside of GitLab
- Long-term issues' data analysis with multiple snapshots created along the time - Long-term issues' data analysis with multiple snapshots created along the time
- Use the long-term data to gather relevant feedback given in the issues, and improve your product based on real metrics - Use the long-term data to gather relevant feedback given in the issues, and improve your product based on real metrics
......
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-03 12:30-0400\n" "PO-Revision-Date: 2017-11-28 11:32-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
...@@ -2070,7 +2070,7 @@ msgid "Time until first merge request" ...@@ -2070,7 +2070,7 @@ msgid "Time until first merge request"
msgstr "Zeit bis zum ersten Merge Request" msgstr "Zeit bis zum ersten Merge Request"
msgid "Timeago|%s days ago" msgid "Timeago|%s days ago"
msgstr "" msgstr "vor %s Tagen"
msgid "Timeago|%s days remaining" msgid "Timeago|%s days remaining"
msgstr "%s Tage verbleibend" msgstr "%s Tage verbleibend"
......
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-03 12:30-0400\n" "PO-Revision-Date: 2017-11-21 16:43-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"
...@@ -36,8 +36,8 @@ msgstr "%{commit_author_link} a validé %{commit_timeago}" ...@@ -36,8 +36,8 @@ msgstr "%{commit_author_link} a validé %{commit_timeago}"
msgid "%{count} participant" msgid "%{count} participant"
msgid_plural "%{count} participants" msgid_plural "%{count} participants"
msgstr[0] "" msgstr[0] "%{count} participant•e"
msgstr[1] "" msgstr[1] "%{count} participant•e•s"
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} validations de retard sur %{default_branch}, %{number_commits_ahead} validations d'avance" msgstr "%{number_commits_behind} validations de retard sur %{default_branch}, %{number_commits_ahead} validations d'avance"
...@@ -60,10 +60,10 @@ msgid "(checkout the %{link} for information on how to install it)." ...@@ -60,10 +60,10 @@ msgid "(checkout the %{link} for information on how to install it)."
msgstr "(Lisez %{link} pour savoir comment l'installer)." msgstr "(Lisez %{link} pour savoir comment l'installer)."
msgid "+ %{moreCount} more" msgid "+ %{moreCount} more"
msgstr "" msgstr "+ %{moreCount} de plus"
msgid "- show less" msgid "- show less"
msgstr "" msgstr "- en montrer moins"
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
...@@ -125,7 +125,7 @@ msgid "AdminHealthPageLink|health page" ...@@ -125,7 +125,7 @@ msgid "AdminHealthPageLink|health page"
msgstr "État des services" msgstr "État des services"
msgid "Advanced settings" msgid "Advanced settings"
msgstr "" msgstr "Paramètres avancés"
msgid "All" msgid "All"
msgstr "Tous" msgstr "Tous"
...@@ -625,8 +625,8 @@ msgstr[1] "Validations" ...@@ -625,8 +625,8 @@ msgstr[1] "Validations"
msgid "Commit %d file" msgid "Commit %d file"
msgid_plural "Commit %d files" msgid_plural "Commit %d files"
msgstr[0] "" msgstr[0] "Valider %d fichier"
msgstr[1] "" msgstr[1] "Valider %d fichiers"
msgid "Commit Message" msgid "Commit Message"
msgstr "Message de validation" msgstr "Message de validation"
...@@ -692,10 +692,10 @@ msgid "ContainerRegistry|Size" ...@@ -692,10 +692,10 @@ msgid "ContainerRegistry|Size"
msgstr "Taille" msgstr "Taille"
msgid "ContainerRegistry|Tag" msgid "ContainerRegistry|Tag"
msgstr "Étiquette" msgstr "Tag"
msgid "ContainerRegistry|Tag ID" msgid "ContainerRegistry|Tag ID"
msgstr "ID d‘étiquette" msgstr "ID du tag"
msgid "ContainerRegistry|Use different image names" msgid "ContainerRegistry|Use different image names"
msgstr "Utilisez des noms d’images différents" msgstr "Utilisez des noms d’images différents"
...@@ -704,7 +704,7 @@ msgid "ContainerRegistry|With the Docker Container Registry integrated into GitL ...@@ -704,7 +704,7 @@ msgid "ContainerRegistry|With the Docker Container Registry integrated into GitL
msgstr "Avec le registre de conteneur Docker intégré à GitLab, chaque projet peut avoir son propre espace pour stocker ses images Docker." msgstr "Avec le registre de conteneur Docker intégré à GitLab, chaque projet peut avoir son propre espace pour stocker ses images Docker."
msgid "Contribution guide" msgid "Contribution guide"
msgstr "Guilde de contribution" msgstr "Guide de contribution"
msgid "Contributors" msgid "Contributors"
msgstr "Contributeurs" msgstr "Contributeurs"
...@@ -737,7 +737,7 @@ msgid "Create empty bare repository" ...@@ -737,7 +737,7 @@ msgid "Create empty bare repository"
msgstr "Créer un dépôt vide" msgstr "Créer un dépôt vide"
msgid "Create file" msgid "Create file"
msgstr "" msgstr "Créer un fichier"
msgid "Create merge request" msgid "Create merge request"
msgstr "Créer une demande de fusion" msgstr "Créer une demande de fusion"
...@@ -746,10 +746,10 @@ msgid "Create new branch" ...@@ -746,10 +746,10 @@ msgid "Create new branch"
msgstr "Créer une nouvelle branche" msgstr "Créer une nouvelle branche"
msgid "Create new directory" msgid "Create new directory"
msgstr "" msgstr "Créer un nouveau dossier"
msgid "Create new file" msgid "Create new file"
msgstr "" msgstr "Créer un nouveau fichier"
msgid "Create new..." msgid "Create new..."
msgstr "Créer nouveau..." msgstr "Créer nouveau..."
...@@ -922,7 +922,7 @@ msgid "Failed to remove the pipeline schedule" ...@@ -922,7 +922,7 @@ msgid "Failed to remove the pipeline schedule"
msgstr "Échec de la suppression du pipeline programmé" msgstr "Échec de la suppression du pipeline programmé"
msgid "File name" msgid "File name"
msgstr "" msgstr "Nom du fichier"
msgid "Files" msgid "Files"
msgstr "Fichiers" msgstr "Fichiers"
...@@ -1364,10 +1364,10 @@ msgid "Notifications" ...@@ -1364,10 +1364,10 @@ msgid "Notifications"
msgstr "Notifications" msgstr "Notifications"
msgid "Number of access attempts" msgid "Number of access attempts"
msgstr "" msgstr "Nombre de tentatives d'accès"
msgid "Number of failures before backing off" msgid "Number of failures before backing off"
msgstr "" msgstr "Nombre d'échecs avant annulation"
msgid "OfSearchInADropdown|Filter" msgid "OfSearchInADropdown|Filter"
msgstr "Filtre" msgstr "Filtre"
...@@ -1625,7 +1625,7 @@ msgid "ProjectSettings|This setting will be applied to all projects unless overr ...@@ -1625,7 +1625,7 @@ msgid "ProjectSettings|This setting will be applied to all projects unless overr
msgstr "Ce paramètre s’appliquera à tous les projets à moins qu’un administrateur ne le modifie." msgstr "Ce paramètre s’appliquera à tous les projets à moins qu’un administrateur ne le modifie."
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails." msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr "" msgstr "Les utilisateurs peuvent uniquement pousser sur ce dépôt des validations qui ont été validées avec une de leurs adresses courriels vérifiées."
msgid "Projects" msgid "Projects"
msgstr "Projets" msgstr "Projets"
...@@ -1664,7 +1664,7 @@ msgid "Push events" ...@@ -1664,7 +1664,7 @@ msgid "Push events"
msgstr "Évènements de poussée" msgstr "Évènements de poussée"
msgid "PushRule|Committer restriction" msgid "PushRule|Committer restriction"
msgstr "" msgstr "Restriction du validateur"
msgid "Read more" msgid "Read more"
msgstr "Lire plus" msgstr "Lire plus"
...@@ -1682,7 +1682,7 @@ msgid "Registry" ...@@ -1682,7 +1682,7 @@ msgid "Registry"
msgstr "Registre" msgstr "Registre"
msgid "Related Commits" msgid "Related Commits"
msgstr "Validations liés" msgstr "Validations liées"
msgid "Related Deployed Jobs" msgid "Related Deployed Jobs"
msgstr "Tâches de déploiement liées" msgstr "Tâches de déploiement liées"
...@@ -1730,7 +1730,7 @@ msgid "SSH Keys" ...@@ -1730,7 +1730,7 @@ msgid "SSH Keys"
msgstr "Clés SSH" msgstr "Clés SSH"
msgid "Save" msgid "Save"
msgstr "" msgstr "Enregistrer"
msgid "Save changes" msgid "Save changes"
msgstr "Enregistrer les modifications" msgstr "Enregistrer les modifications"
...@@ -1939,10 +1939,10 @@ msgid "Subgroups" ...@@ -1939,10 +1939,10 @@ msgid "Subgroups"
msgstr "Sous-groupes" msgstr "Sous-groupes"
msgid "Subscribe" msgid "Subscribe"
msgstr "" msgstr "S’abonner"
msgid "Switch branch/tag" msgid "Switch branch/tag"
msgstr "Changer de branche / d'étiquette" msgstr "Changer de branche / tag"
msgid "System Hooks" msgid "System Hooks"
msgstr "Crochets système" msgstr "Crochets système"
...@@ -1968,7 +1968,7 @@ msgid "The Advanced Global Search in GitLab is a powerful search service that sa ...@@ -1968,7 +1968,7 @@ msgid "The Advanced Global Search in GitLab is a powerful search service that sa
msgstr "La Recherche Globale Avancée de GitLab est un outils qui vous fait gagner du temps. Au lieu de créer du code similaire et perdre du temps, vous pouvez maintenant chercher dans le code d'autres équipes pour vous aider sur votre projet." msgstr "La Recherche Globale Avancée de GitLab est un outils qui vous fait gagner du temps. Au lieu de créer du code similaire et perdre du temps, vous pouvez maintenant chercher dans le code d'autres équipes pour vous aider sur votre projet."
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold" msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "" msgstr "Le seuil d’interruption du disjoncteur devrait être inférieur au seuil de nombre de défaillance"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion." msgstr "L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion."
...@@ -1983,10 +1983,10 @@ msgid "The issue stage shows the time it takes from creating an issue to assigni ...@@ -1983,10 +1983,10 @@ msgid "The issue stage shows the time it takes from creating an issue to assigni
msgstr "L'étape des tickets montre le temps nécessaire entre la création d'un ticket et son assignation à un jalon, ou son ajout à une liste d'un tableau de tickets. Commencez par créer des tickets pour voir des données pour cette étape." msgstr "L'étape des tickets montre le temps nécessaire entre la création d'un ticket et son assignation à un jalon, ou son ajout à une liste d'un tableau de tickets. Commencez par créer des tickets pour voir des données pour cette étape."
msgid "The number of attempts GitLab will make to access a storage." msgid "The number of attempts GitLab will make to access a storage."
msgstr "" msgstr "Le nombre de tentatives que GitLab va effectuer pour accéder au stockage."
msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host" msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
msgstr "" msgstr "Le nombre d'échecs avant que GitLab ne commence à désactiver l'accès à la partition de stockage sur l'hôte"
msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}." msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
msgstr "Nombre d’échecs avant que GitLab n’empêche tout accès au stockage. Ce nombre d’échecs peut être réinitialisé dans l’interface d’administration : %{link_to_health_page} ou en suivant le %{api_documentation_link}." msgstr "Nombre d’échecs avant que GitLab n’empêche tout accès au stockage. Ce nombre d’échecs peut être réinitialisé dans l’interface d’administration : %{link_to_health_page} ou en suivant le %{api_documentation_link}."
...@@ -2224,7 +2224,7 @@ msgid "Unstar" ...@@ -2224,7 +2224,7 @@ msgid "Unstar"
msgstr "Supprimer des favoris" msgstr "Supprimer des favoris"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "" msgstr "Se désabonner"
msgid "Upgrade your plan to activate Advanced Global Search." msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "Mettez à jour votre abonnement pour activer la Recherche Globale Avancée." msgstr "Mettez à jour votre abonnement pour activer la Recherche Globale Avancée."
......
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-03 12:31-0400\n" "PO-Revision-Date: 2017-11-20 03:59-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Language: it_IT\n" "Language: it_IT\n"
...@@ -2085,7 +2085,7 @@ msgid "Timeago|%s minutes remaining" ...@@ -2085,7 +2085,7 @@ msgid "Timeago|%s minutes remaining"
msgstr "%s minuti rimanenti" msgstr "%s minuti rimanenti"
msgid "Timeago|%s months ago" msgid "Timeago|%s months ago"
msgstr "%s minuti fa" msgstr "%s mesi fa"
msgid "Timeago|%s months remaining" msgid "Timeago|%s months remaining"
msgstr "%s mesi rimanenti" msgstr "%s mesi rimanenti"
......
This diff is collapsed.
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-04 13:27-0400\n" "PO-Revision-Date: 2017-11-18 12:51-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n" "Language: pt_BR\n"
...@@ -361,10 +361,10 @@ msgid "Browse Directory" ...@@ -361,10 +361,10 @@ msgid "Browse Directory"
msgstr "Navegar no Diretório" msgstr "Navegar no Diretório"
msgid "Browse File" msgid "Browse File"
msgstr "Pesquisar Arquivo" msgstr "Acessar arquivo"
msgid "Browse Files" msgid "Browse Files"
msgstr "Pesquisar Arquivos" msgstr "Acessar arquivos"
msgid "Browse files" msgid "Browse files"
msgstr "Navegar pelos arquivos" msgstr "Navegar pelos arquivos"
...@@ -1367,7 +1367,7 @@ msgid "Number of access attempts" ...@@ -1367,7 +1367,7 @@ msgid "Number of access attempts"
msgstr "Número de tentativas de acesso" msgstr "Número de tentativas de acesso"
msgid "Number of failures before backing off" msgid "Number of failures before backing off"
msgstr "" msgstr "Número de falhas antes de reverter"
msgid "OfSearchInADropdown|Filter" msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar" msgstr "Filtrar"
...@@ -1968,7 +1968,7 @@ msgid "The Advanced Global Search in GitLab is a powerful search service that sa ...@@ -1968,7 +1968,7 @@ msgid "The Advanced Global Search in GitLab is a powerful search service that sa
msgstr "A pesquisa global avançado no GitLab é um serviço poderoso de pesquisa que poupa seu tempo. Ao invés de criar código duplicado e perder seu tempo, você pode agora pesquisar por código de outros times que podem ajudar no seu próprio projeto." msgstr "A pesquisa global avançado no GitLab é um serviço poderoso de pesquisa que poupa seu tempo. Ao invés de criar código duplicado e perder seu tempo, você pode agora pesquisar por código de outros times que podem ajudar no seu próprio projeto."
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold" msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "" msgstr "O limite do recuso do circuitbreaker deve ser inferior ao limite de contagem de falhas"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "A etapa de codificação mostra o tempo desde a entrega do primeiro commit até a criação do merge request. Os dados serão automaticamente adicionados aqui desde o momento de criação do merge request." msgstr "A etapa de codificação mostra o tempo desde a entrega do primeiro commit até a criação do merge request. Os dados serão automaticamente adicionados aqui desde o momento de criação do merge request."
......
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-05 14:39-0500\n" "PO-Revision-Date: 2017-11-17 07:55-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru_RU\n" "Language: ru_RU\n"
...@@ -188,7 +188,7 @@ msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly ...@@ -188,7 +188,7 @@ msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly
msgstr "Приложения для автоматического ревью и автоматического развёртывания требуют указания %{kubernetes} для корректной работы." msgstr "Приложения для автоматического ревью и автоматического развёртывания требуют указания %{kubernetes} для корректной работы."
msgid "AutoDevOps|Auto DevOps (Beta)" msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr "" msgstr "Auto DevOps (бета)"
msgid "AutoDevOps|Auto DevOps documentation" msgid "AutoDevOps|Auto DevOps documentation"
msgstr "" msgstr ""
......
This diff is collapsed.
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-06 01:25-0500\n" "PO-Revision-Date: 2017-11-23 02:44-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n" "Language-Team: Chinese Simplified\n"
"Language: zh_CN\n" "Language: zh_CN\n"
...@@ -656,10 +656,10 @@ msgid "ContainerRegistry|Created" ...@@ -656,10 +656,10 @@ msgid "ContainerRegistry|Created"
msgstr "已创建" msgstr "已创建"
msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:" msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
msgstr "首先使用您的 GitLab 用户名和密码登录 GitLab 的容器注册表。如果您有%{link_2fa},您需要使用%{link_token}:" msgstr "首先使用您的 GitLab 用户名和密码登录 GitLab 的容器注册表。如果您已经%{link_2fa},则需要使用%{link_token}:"
msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:" msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
msgstr "GitLab 最多支持3个级别的镜像名称。以下镜像示例对您的项目有效:" msgstr "GitLab 最多支持3个级别的镜像命名。以下镜像名称示例对当前项目有效:"
msgid "ContainerRegistry|How to use the Container Registry" msgid "ContainerRegistry|How to use the Container Registry"
msgstr "如何使用容器注册表" msgstr "如何使用容器注册表"
...@@ -671,7 +671,7 @@ msgid "ContainerRegistry|No tags in Container Registry for this container image. ...@@ -671,7 +671,7 @@ msgid "ContainerRegistry|No tags in Container Registry for this container image.
msgstr "容器注册表中没有此容器镜像的标签。" msgstr "容器注册表中没有此容器镜像的标签。"
msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands" msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
msgstr "登录后您可以使用通用的%{build}和%{push}命令自由创建和上传容器映像" msgstr "登录后您可以使用通用的%{build}和%{push}命令创建和上传容器镜像"
msgid "ContainerRegistry|Remove repository" msgid "ContainerRegistry|Remove repository"
msgstr "删除存储库" msgstr "删除存储库"
...@@ -692,7 +692,7 @@ msgid "ContainerRegistry|Use different image names" ...@@ -692,7 +692,7 @@ msgid "ContainerRegistry|Use different image names"
msgstr "使用不同的镜像名称" msgstr "使用不同的镜像名称"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr "将 Docker 容器注册表集成到 GitLab 中,每个项目都可以有自己的空间来存储 Docker 的图像。" msgstr "将 Docker 容器注册表集成到 GitLab 中,每个项目都可以有各自的空间来存储 Docker 的镜像。"
msgid "Contribution guide" msgid "Contribution guide"
msgstr "贡献指南" msgstr "贡献指南"
...@@ -1155,7 +1155,7 @@ msgid "Last update" ...@@ -1155,7 +1155,7 @@ msgid "Last update"
msgstr "最后更新" msgstr "最后更新"
msgid "Last updated" msgid "Last updated"
msgstr "最更新" msgstr "最更新"
msgid "LastPushEvent|You pushed to" msgid "LastPushEvent|You pushed to"
msgstr "您推送了" msgstr "您推送了"
...@@ -1274,7 +1274,7 @@ msgid "New tag" ...@@ -1274,7 +1274,7 @@ msgid "New tag"
msgstr "新建标签" msgstr "新建标签"
msgid "No container images stored for this project. Add one by following the instructions above." msgid "No container images stored for this project. Add one by following the instructions above."
msgstr "没有为此项目存储容器镜像。请按照上述说明添加一个。" msgstr "此项目当前未存储容器镜像。如需使用,请参照上述说明新建容器镜像。"
msgid "No repository" msgid "No repository"
msgstr "没有存储库" msgstr "没有存储库"
...@@ -1361,7 +1361,7 @@ msgid "Only project members can comment." ...@@ -1361,7 +1361,7 @@ msgid "Only project members can comment."
msgstr "只有项目成员可以发表评论。" msgstr "只有项目成员可以发表评论。"
msgid "OpenedNDaysAgo|Opened" msgid "OpenedNDaysAgo|Opened"
msgstr "开始于" msgstr "创建于"
msgid "Opens in a new window" msgid "Opens in a new window"
msgstr "打开一个新窗口" msgstr "打开一个新窗口"
...@@ -1775,10 +1775,10 @@ msgid "Settings" ...@@ -1775,10 +1775,10 @@ msgid "Settings"
msgstr "设置" msgstr "设置"
msgid "Show parent pages" msgid "Show parent pages"
msgstr "查看页面" msgstr "查看上级页面"
msgid "Show parent subgroups" msgid "Show parent subgroups"
msgstr "查看群组中的子群组" msgstr "查看上级子群组"
msgid "Showing %d event" msgid "Showing %d event"
msgid_plural "Showing %d events" msgid_plural "Showing %d events"
...@@ -1830,13 +1830,13 @@ msgid "SortOptions|Largest repository" ...@@ -1830,13 +1830,13 @@ msgid "SortOptions|Largest repository"
msgstr "最大存储库" msgstr "最大存储库"
msgid "SortOptions|Last created" msgid "SortOptions|Last created"
msgstr "最创建" msgstr "最创建"
msgid "SortOptions|Last joined" msgid "SortOptions|Last joined"
msgstr "最新加入" msgstr "最新加入"
msgid "SortOptions|Last updated" msgid "SortOptions|Last updated"
msgstr "最更新" msgstr "最更新"
msgid "SortOptions|Least popular" msgid "SortOptions|Least popular"
msgstr "最不受欢迎" msgstr "最不受欢迎"
...@@ -1869,7 +1869,7 @@ msgid "SortOptions|Name, descending" ...@@ -1869,7 +1869,7 @@ msgid "SortOptions|Name, descending"
msgstr "名称,降序排列" msgstr "名称,降序排列"
msgid "SortOptions|Oldest created" msgid "SortOptions|Oldest created"
msgstr "最早创建" msgstr "最早创建"
msgid "SortOptions|Oldest joined" msgid "SortOptions|Oldest joined"
msgstr "最早的加入" msgstr "最早的加入"
...@@ -1878,7 +1878,7 @@ msgid "SortOptions|Oldest sign in" ...@@ -1878,7 +1878,7 @@ msgid "SortOptions|Oldest sign in"
msgstr "最早的登录" msgstr "最早的登录"
msgid "SortOptions|Oldest updated" msgid "SortOptions|Oldest updated"
msgstr "最早的提交" msgstr "最早更新"
msgid "SortOptions|Popularity" msgid "SortOptions|Popularity"
msgstr "人气" msgstr "人气"
...@@ -1945,7 +1945,7 @@ msgid "Team" ...@@ -1945,7 +1945,7 @@ msgid "Team"
msgstr "团队" msgstr "团队"
msgid "Thanks! Don't show me this again" msgid "Thanks! Don't show me this again"
msgstr "谢谢 ! 请不要再显示" msgstr "不再显示该提示"
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project." msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr "GitLab 中的高级全局搜索功能是非常强大的搜索服务。您可以搜索其他团队的代码以帮助您完善自己项目中的代码。从而避免创建重复的代码或浪费时间。" msgstr "GitLab 中的高级全局搜索功能是非常强大的搜索服务。您可以搜索其他团队的代码以帮助您完善自己项目中的代码。从而避免创建重复的代码或浪费时间。"
...@@ -2465,7 +2465,7 @@ msgstr "通知邮件" ...@@ -2465,7 +2465,7 @@ msgstr "通知邮件"
msgid "parent" msgid "parent"
msgid_plural "parents" msgid_plural "parents"
msgstr[0] "级" msgstr[0] "级"
msgid "password" msgid "password"
msgstr "密码" msgstr "密码"
......
...@@ -3,7 +3,7 @@ msgstr "" ...@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n" "Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-11-02 14:42+0100\n"
"PO-Revision-Date: 2017-11-03 12:30-0400\n" "PO-Revision-Date: 2017-11-15 02:54-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n" "Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n" "Language: zh_HK\n"
...@@ -609,7 +609,7 @@ msgid "ClusterIntegration|properly configured" ...@@ -609,7 +609,7 @@ msgid "ClusterIntegration|properly configured"
msgstr "" msgstr ""
msgid "Comments" msgid "Comments"
msgstr "評論 (Comment)" msgstr "評論"
msgid "Commit" msgid "Commit"
msgid_plural "Commits" msgid_plural "Commits"
...@@ -876,7 +876,7 @@ msgid "EventFilterBy|Filter by all" ...@@ -876,7 +876,7 @@ msgid "EventFilterBy|Filter by all"
msgstr "全部" msgstr "全部"
msgid "EventFilterBy|Filter by comments" msgid "EventFilterBy|Filter by comments"
msgstr "按評論 (comment) 過濾" msgstr "按評論過濾"
msgid "EventFilterBy|Filter by issue events" msgid "EventFilterBy|Filter by issue events"
msgstr "按議題事件 (issue event) 過濾" msgstr "按議題事件 (issue event) 過濾"
......
This diff is collapsed.
...@@ -130,11 +130,35 @@ describe Projects::BranchesController do ...@@ -130,11 +130,35 @@ describe Projects::BranchesController do
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
end end
end end
<<<<<<< HEAD
context 'when user configured kubernetes from Integration > Kubernetes' do context 'when user configured kubernetes from Integration > Kubernetes' do
before do before do
project.services << build(:kubernetes_service) project.services << build(:kubernetes_service)
end end
=======
context 'when user configured kubernetes from Integration > Kubernetes' do
before do
project.services << build(:kubernetes_service)
end
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
context 'when user configured kubernetes from CI/CD > Clusters' do
before do
create(:cluster, :provided_by_gcp, projects: [project])
end
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
it 'redirects to autodeploy setup page' do
result = { status: :success, branch: double(name: branch) }
create(:cluster, :provided_by_gcp, projects: [project])
>>>>>>> origin/master
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end end
......
...@@ -34,7 +34,7 @@ describe Projects::EnvironmentsController do ...@@ -34,7 +34,7 @@ describe Projects::EnvironmentsController do
context 'when requesting JSON response for folders' do context 'when requesting JSON response for folders' do
before do before do
allow_any_instance_of(Environment).to receive(:deployment_service_ready?).and_return(true) allow_any_instance_of(Environment).to receive(:has_terminals?).and_return(true)
allow_any_instance_of(Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status) allow_any_instance_of(Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
create(:environment, project: project, create(:environment, project: project,
......
...@@ -43,6 +43,17 @@ describe 'Epic Issues', :js do ...@@ -43,6 +43,17 @@ describe 'Epic Issues', :js do
end end
context 'when user is a group member' do context 'when user is a group member' do
let(:issue_to_add) { create(:issue, project: private_project) }
let(:issue_invalid) { create(:issue) }
def add_issues(references)
find('.related-issues-block h3.panel-title button').click
find('.js-add-issuable-form-input').set(references)
find('.js-add-issuable-form-add-button').click
wait_for_requests
end
before do before do
group.add_developer(user) group.add_developer(user)
visit_epic visit_epic
...@@ -64,16 +75,20 @@ describe 'Epic Issues', :js do ...@@ -64,16 +75,20 @@ describe 'Epic Issues', :js do
end end
end end
it 'user cannot add new issues to the epic from another group' do
add_issues("#{issue_invalid.to_reference(full: true)}")
expect(page).to have_selector('.content-wrapper .alert-wrapper .flash-text')
expect(find('.flash-alert')).to have_text('No Issue found for given params')
end
it 'user can add new issues to the epic' do it 'user can add new issues to the epic' do
issue_to_add = create(:issue, project: private_project)
issue_invalid = create(:issue)
references = "#{issue_to_add.to_reference(full: true)} #{issue_invalid.to_reference(full: true)}" references = "#{issue_to_add.to_reference(full: true)} #{issue_invalid.to_reference(full: true)}"
find('.related-issues-block h3.panel-title button').click add_issues(references)
find('.js-add-issuable-form-input').set references
find('.js-add-issuable-form-add-button').click
wait_for_requests expect(page).not_to have_selector('.content-wrapper .alert-wrapper .flash-text')
expect(page).not_to have_content('No Issue found for given params')
within('.related-issues-block ul.issuable-list') do within('.related-issues-block ul.issuable-list') do
expect(page).to have_selector('li', count: 3) expect(page).to have_selector('li', count: 3)
......
...@@ -4,8 +4,8 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching: ...@@ -4,8 +4,8 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching:
include KubernetesHelpers include KubernetesHelpers
include ReactiveCachingHelpers include ReactiveCachingHelpers
let(:project) { build_stubbed(:kubernetes_project) } shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
let(:service) { project.kubernetes_service } let(:service) { project.deployment_platform }
describe '#rollout_status' do describe '#rollout_status' do
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
...@@ -51,4 +51,18 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching: ...@@ -51,4 +51,18 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching:
end end
end end
end end
end
context 'when user configured kubernetes from Integration > Kubernetes' do
let(:project) { create(:kubernetes_project) }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end end
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import Autosize from 'autosize'; import Autosize from 'autosize';
import store from '~/notes/stores'; import store from '~/notes/stores';
import issueCommentForm from '~/notes/components/issue_comment_form.vue'; import issueCommentForm from '~/notes/components/issue_comment_form.vue';
import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data'; import { loggedOutIssueData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers'; import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_comment_form component', () => { describe('issue_comment_form component', () => {
...@@ -23,7 +23,7 @@ describe('issue_comment_form component', () => { ...@@ -23,7 +23,7 @@ describe('issue_comment_form component', () => {
describe('user is logged in', () => { describe('user is logged in', () => {
beforeEach(() => { beforeEach(() => {
store.dispatch('setUserData', userDataMock); store.dispatch('setUserData', userDataMock);
store.dispatch('setIssueData', issueDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
vm = mountComponent(); vm = mountComponent();
...@@ -178,7 +178,7 @@ describe('issue_comment_form component', () => { ...@@ -178,7 +178,7 @@ describe('issue_comment_form component', () => {
describe('issue is confidential', () => { describe('issue is confidential', () => {
it('shows information warning', (done) => { it('shows information warning', (done) => {
store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true })); store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
done(); done();
...@@ -190,7 +190,7 @@ describe('issue_comment_form component', () => { ...@@ -190,7 +190,7 @@ describe('issue_comment_form component', () => {
describe('user is not logged in', () => { describe('user is not logged in', () => {
beforeEach(() => { beforeEach(() => {
store.dispatch('setUserData', null); store.dispatch('setUserData', null);
store.dispatch('setIssueData', loggedOutIssueData); store.dispatch('setNoteableData', loggedOutIssueData);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
vm = mountComponent(); vm = mountComponent();
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
import issueDiscussion from '~/notes/components/issue_discussion.vue'; import issueDiscussion from '~/notes/components/issue_discussion.vue';
import { issueDataMock, discussionMock, notesDataMock } from '../mock_data'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
describe('issue_discussion component', () => { describe('issue_discussion component', () => {
let vm; let vm;
...@@ -9,7 +9,7 @@ describe('issue_discussion component', () => { ...@@ -9,7 +9,7 @@ describe('issue_discussion component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(issueDiscussion); const Component = Vue.extend(issueDiscussion);
store.dispatch('setIssueData', issueDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
vm = new Component({ vm = new Component({
......
...@@ -24,7 +24,7 @@ describe('issue_note_app', () => { ...@@ -24,7 +24,7 @@ describe('issue_note_app', () => {
mountComponent = (data) => { mountComponent = (data) => {
const props = data || { const props = data || {
issueData: mockData.issueDataMock, noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock, notesData: mockData.notesDataMock,
userData: mockData.userDataMock, userData: mockData.userDataMock,
}; };
...@@ -60,7 +60,7 @@ describe('issue_note_app', () => { ...@@ -60,7 +60,7 @@ describe('issue_note_app', () => {
}); });
it('should set issue data', () => { it('should set issue data', () => {
expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock); expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock);
}); });
it('should set user data', () => { it('should set user data', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
import awardsNote from '~/notes/components/issue_note_awards_list.vue'; import awardsNote from '~/notes/components/issue_note_awards_list.vue';
import { issueDataMock, notesDataMock } from '../mock_data'; import { noteableDataMock, notesDataMock } from '../mock_data';
describe('issue_note_awards_list component', () => { describe('issue_note_awards_list component', () => {
let vm; let vm;
...@@ -10,7 +10,7 @@ describe('issue_note_awards_list component', () => { ...@@ -10,7 +10,7 @@ describe('issue_note_awards_list component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(awardsNote); const Component = Vue.extend(awardsNote);
store.dispatch('setIssueData', issueDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
awardsMock = [ awardsMock = [
{ {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
import noteBody from '~/notes/components/issue_note_body.vue'; import noteBody from '~/notes/components/issue_note_body.vue';
import { issueDataMock, notesDataMock, note } from '../mock_data'; import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => { describe('issue_note_body component', () => {
let vm; let vm;
...@@ -10,7 +10,7 @@ describe('issue_note_body component', () => { ...@@ -10,7 +10,7 @@ describe('issue_note_body component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(noteBody); const Component = Vue.extend(noteBody);
store.dispatch('setIssueData', issueDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
vm = new Component({ vm = new Component({
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
import issueNoteForm from '~/notes/components/issue_note_form.vue'; import issueNoteForm from '~/notes/components/issue_note_form.vue';
import { issueDataMock, notesDataMock } from '../mock_data'; import { noteableDataMock, notesDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers'; import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_note_form component', () => { describe('issue_note_form component', () => {
...@@ -11,7 +11,7 @@ describe('issue_note_form component', () => { ...@@ -11,7 +11,7 @@ describe('issue_note_form component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(issueNoteForm); const Component = Vue.extend(issueNoteForm);
store.dispatch('setIssueData', issueDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
props = { props = {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
import issueNote from '~/notes/components/issue_note.vue'; import issueNote from '~/notes/components/issue_note.vue';
import { issueDataMock, notesDataMock, note } from '../mock_data'; import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => { describe('issue_note', () => {
let vm; let vm;
...@@ -10,7 +10,7 @@ describe('issue_note', () => { ...@@ -10,7 +10,7 @@ describe('issue_note', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(issueNote); const Component = Vue.extend(issueNote);
store.dispatch('setIssueData', issueDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
vm = new Component({ vm = new Component({
......
...@@ -18,7 +18,7 @@ export const userDataMock = { ...@@ -18,7 +18,7 @@ export const userDataMock = {
username: 'root', username: 'root',
}; };
export const issueDataMock = { export const noteableDataMock = {
assignees: [], assignees: [],
author_id: 1, author_id: 1,
branch_name: null, branch_name: null,
......
import * as actions from '~/notes/stores/actions'; import * as actions from '~/notes/stores/actions';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => { describe('Actions Notes Store', () => {
describe('setNotesData', () => { describe('setNotesData', () => {
...@@ -11,10 +11,10 @@ describe('Actions Notes Store', () => { ...@@ -11,10 +11,10 @@ describe('Actions Notes Store', () => {
}); });
}); });
describe('setIssueData', () => { describe('setNoteableData', () => {
it('should set received issue data', (done) => { it('should set received issue data', (done) => {
testAction(actions.setIssueData, null, { issueData: {} }, [ testAction(actions.setNoteableData, null, { noteableData: {} }, [
{ type: 'SET_ISSUE_DATA', payload: issueDataMock }, { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock },
], done); ], done);
}); });
}); });
......
import * as getters from '~/notes/stores/getters'; import * as getters from '~/notes/stores/getters';
import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Getters Notes Store', () => { describe('Getters Notes Store', () => {
let state; let state;
...@@ -11,7 +11,7 @@ describe('Getters Notes Store', () => { ...@@ -11,7 +11,7 @@ describe('Getters Notes Store', () => {
notesData: notesDataMock, notesData: notesDataMock,
userData: userDataMock, userData: userDataMock,
issueData: issueDataMock, noteableData: noteableDataMock,
}; };
}); });
describe('notes', () => { describe('notes', () => {
...@@ -32,9 +32,9 @@ describe('Getters Notes Store', () => { ...@@ -32,9 +32,9 @@ describe('Getters Notes Store', () => {
}); });
}); });
describe('getIssueData', () => { describe('getNoteableData', () => {
it('should return all data in `issueData`', () => { it('should return all data in `noteableData`', () => {
expect(getters.getIssueData(state)).toEqual(issueDataMock); expect(getters.getNoteableData(state)).toEqual(noteableDataMock);
}); });
}); });
......
import mutations from '~/notes/stores/mutations'; import mutations from '~/notes/stores/mutations';
import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Mutation Notes Store', () => { describe('Mutation Notes Store', () => {
describe('ADD_NEW_NOTE', () => { describe('ADD_NEW_NOTE', () => {
...@@ -74,14 +74,14 @@ describe('Mutation Notes Store', () => { ...@@ -74,14 +74,14 @@ describe('Mutation Notes Store', () => {
}); });
}); });
describe('SET_ISSUE_DATA', () => { describe('SET_NOTEABLE_DATA', () => {
it('should set the issue data', () => { it('should set the issue data', () => {
const state = { const state = {
issueData: {}, noteableData: {},
}; };
mutations.SET_ISSUE_DATA(state, issueDataMock); mutations.SET_NOTEABLE_DATA(state, noteableDataMock);
expect(state.issueData).toEqual(issueDataMock); expect(state.noteableData).toEqual(noteableDataMock);
}); });
}); });
......
...@@ -239,17 +239,31 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -239,17 +239,31 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
<<<<<<< HEAD
context 'when kubernetes responds with valid pods' do context 'when kubernetes responds with valid pods' do
before do before do
stub_kubeclient_pods stub_kubeclient_pods
end end
it { is_expected.to eq(pods: [kube_pod]) } it { is_expected.to eq(pods: [kube_pod]) }
=======
context 'when kubernetes responds with valid pods and deployments' do
before do
stub_kubeclient_pods
stub_kubeclient_deployments
end
it { is_expected.to eq(pods: [kube_pod], deployments: [kube_deployment]) }
>>>>>>> origin/master
end end
context 'when kubernetes responds with 500s' do context 'when kubernetes responds with 500s' do
before do before do
stub_kubeclient_pods(status: 500) stub_kubeclient_pods(status: 500)
<<<<<<< HEAD
=======
stub_kubeclient_deployments(status: 500)
>>>>>>> origin/master
end end
it { expect { subject }.to raise_error(KubeException) } it { expect { subject }.to raise_error(KubeException) }
...@@ -258,9 +272,16 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -258,9 +272,16 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do context 'when kubernetes responds with 404s' do
before do before do
stub_kubeclient_pods(status: 404) stub_kubeclient_pods(status: 404)
<<<<<<< HEAD
end end
it { is_expected.to eq(pods: []) } it { is_expected.to eq(pods: []) }
=======
stub_kubeclient_deployments(status: 404)
end
it { is_expected.to eq(pods: [], deployments: []) }
>>>>>>> origin/master
end end
end end
end end
...@@ -322,8 +322,8 @@ describe Environment do ...@@ -322,8 +322,8 @@ describe Environment do
end end
end end
describe '#deployment_service_ready?' do describe '#has_terminals?' do
subject { environment.deployment_service_ready? } subject { environment.has_terminals? }
context 'when the enviroment is available' do context 'when the enviroment is available' do
context 'with a deployment service' do context 'with a deployment service' do
...@@ -373,7 +373,7 @@ describe Environment do ...@@ -373,7 +373,7 @@ describe Environment do
context 'when the environment has terminals' do context 'when the environment has terminals' do
before do before do
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:has_terminals?).and_return(true)
end end
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
...@@ -386,6 +386,29 @@ describe Environment do ...@@ -386,6 +386,29 @@ describe Environment do
end end
end end
<<<<<<< HEAD
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
it 'returns the terminals from the deployment service' do
expect(project.deployment_platform)
.to receive(:terminals).with(environment)
.and_return(:fake_terminals)
is_expected.to eq(:fake_terminals)
end
end
context 'when user configured kubernetes from Integration > Kubernetes' do
let(:project) { create(:kubernetes_project) }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
=======
context 'when user configured kubernetes from Integration > Kubernetes' do context 'when user configured kubernetes from Integration > Kubernetes' do
let(:project) { create(:kubernetes_project) } let(:project) { create(:kubernetes_project) }
...@@ -397,12 +420,13 @@ describe Environment do ...@@ -397,12 +420,13 @@ describe Environment do
let(:project) { cluster.project } let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
>>>>>>> origin/master
end end
end end
context 'when the environment does not have terminals' do context 'when the environment does not have terminals' do
before do before do
allow(environment).to receive(:deployment_service_ready?).and_return(false) allow(environment).to receive(:has_terminals?).and_return(false)
end end
it { is_expected.to be_nil } it { is_expected.to be_nil }
...@@ -410,13 +434,12 @@ describe Environment do ...@@ -410,13 +434,12 @@ describe Environment do
end end
describe '#rollout_status' do describe '#rollout_status' do
let(:project) { create(:kubernetes_project) } shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
subject { environment.rollout_status } subject { environment.rollout_status }
context 'when the environment has rollout status' do context 'when the environment has rollout status' do
before do before do
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:has_terminals?).and_return(true)
end end
it 'returns the rollout status from the deployment service' do it 'returns the rollout status from the deployment service' do
...@@ -428,15 +451,36 @@ describe Environment do ...@@ -428,15 +451,36 @@ describe Environment do
end end
end end
<<<<<<< HEAD
it 'returns the rollout status from the deployment service' do
expect(project.deployment_platform)
.to receive(:rollout_status).with(environment)
.and_return(:fake_rollout_status)
=======
context 'when the environment does not have rollout status' do context 'when the environment does not have rollout status' do
before do before do
allow(environment).to receive(:deployment_service_ready?).and_return(false) allow(environment).to receive(:has_terminals?).and_return(false)
end end
>>>>>>> origin/master
it { is_expected.to eq(nil) } it { is_expected.to eq(nil) }
end end
end end
context 'when user configured kubernetes from Integration > Kubernetes' do
let(:project) { create(:kubernetes_project) }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
describe '#has_metrics?' do describe '#has_metrics?' do
subject { environment.has_metrics? } subject { environment.has_metrics? }
......
...@@ -51,7 +51,7 @@ describe EnvironmentEntity do ...@@ -51,7 +51,7 @@ describe EnvironmentEntity do
context 'with deployment service ready' do context 'with deployment service ready' do
before do before do
stub_licensed_features(deploy_board: true) stub_licensed_features(deploy_board: true)
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:has_terminals?).and_return(true)
allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status) allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
end end
...@@ -63,7 +63,7 @@ describe EnvironmentEntity do ...@@ -63,7 +63,7 @@ describe EnvironmentEntity do
context 'when license does not has the GitLab_DeployBoard add-on' do context 'when license does not has the GitLab_DeployBoard add-on' do
before do before do
stub_licensed_features(deploy_board: false) stub_licensed_features(deploy_board: false)
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:has_terminals?).and_return(true)
end end
it 'does not expose rollout_status' do it 'does not expose rollout_status' do
......
...@@ -28,7 +28,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -28,7 +28,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
allow(Gitlab::Geo).to receive(:geo_database_configured?) { false } allow(Gitlab::Geo).to receive(:geo_database_configured?) { false }
expect(GeoFileDownloadWorker).not_to receive(:perform_async) expect(Geo::FileDownloadWorker).not_to receive(:perform_async)
subject.perform subject.perform
...@@ -43,7 +43,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -43,7 +43,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
secondary.enabled = false secondary.enabled = false
secondary.save secondary.save
expect(GeoFileDownloadWorker).not_to receive(:perform_async) expect(Geo::FileDownloadWorker).not_to receive(:perform_async)
subject.perform subject.perform
end end
...@@ -58,8 +58,8 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -58,8 +58,8 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
end end
it 'filters S3-backed files' do it 'filters S3-backed files' do
expect(GeoFileDownloadWorker).to receive(:perform_async).with(:lfs, lfs_object_local_store.id) expect(Geo::FileDownloadWorker).to receive(:perform_async).with(:lfs, lfs_object_local_store.id)
expect(GeoFileDownloadWorker).not_to receive(:perform_async).with(:lfs, lfs_object_remote_store.id) expect(Geo::FileDownloadWorker).not_to receive(:perform_async).with(:lfs, lfs_object_remote_store.id)
subject.perform subject.perform
end end
...@@ -81,7 +81,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -81,7 +81,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
create_list(:upload, 2, :personal_snippet) create_list(:upload, 2, :personal_snippet)
create(:appearance, logo: avatar, header_logo: avatar) create(:appearance, logo: avatar, header_logo: avatar)
expect(GeoFileDownloadWorker).to receive(:perform_async).exactly(10).times.and_call_original expect(Geo::FileDownloadWorker).to receive(:perform_async).exactly(10).times.and_call_original
# For 10 downloads, we expect four database reloads: # For 10 downloads, we expect four database reloads:
# 1. Load the first batch of 5. # 1. Load the first batch of 5.
# 2. 4 get sent out, 1 remains. This triggers another reload, which loads in the next 5. # 2. 4 get sent out, 1 remains. This triggers another reload, which loads in the next 5.
...@@ -103,14 +103,14 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -103,14 +103,14 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
stub_const('Geo::BaseSchedulerWorker::DB_RETRIEVE_BATCH_SIZE', 1) stub_const('Geo::BaseSchedulerWorker::DB_RETRIEVE_BATCH_SIZE', 1)
expect(GeoFileDownloadWorker).not_to receive(:perform_async).with(:lfs, failed_registry.file_id) expect(Geo::FileDownloadWorker).not_to receive(:perform_async).with(:lfs, failed_registry.file_id)
expect(GeoFileDownloadWorker).to receive(:perform_async).with(:lfs, unsynced.id) expect(Geo::FileDownloadWorker).to receive(:perform_async).with(:lfs, unsynced.id)
subject.perform subject.perform
end end
it 'retries failed files' do it 'retries failed files' do
expect(GeoFileDownloadWorker).to receive(:perform_async).with('lfs', failed_registry.file_id) expect(Geo::FileDownloadWorker).to receive(:perform_async).with('lfs', failed_registry.file_id)
subject.perform subject.perform
end end
...@@ -118,7 +118,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -118,7 +118,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
it 'does not retries failed files when retry_at is tomorrow' do it 'does not retries failed files when retry_at is tomorrow' do
failed_registry = create(:geo_file_registry, :lfs, file_id: 999, success: false, retry_at: Date.tomorrow) failed_registry = create(:geo_file_registry, :lfs, file_id: 999, success: false, retry_at: Date.tomorrow)
expect(GeoFileDownloadWorker).not_to receive(:perform_async).with('lfs', failed_registry.file_id) expect(Geo::FileDownloadWorker).not_to receive(:perform_async).with('lfs', failed_registry.file_id)
subject.perform subject.perform
end end
...@@ -126,7 +126,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -126,7 +126,7 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
it 'does not retries failed files when retry_at is in the past' do it 'does not retries failed files when retry_at is in the past' do
failed_registry = create(:geo_file_registry, :lfs, file_id: 999, success: false, retry_at: Date.yesterday) failed_registry = create(:geo_file_registry, :lfs, file_id: 999, success: false, retry_at: Date.yesterday)
expect(GeoFileDownloadWorker).to receive(:perform_async).with('lfs', failed_registry.file_id) expect(Geo::FileDownloadWorker).to receive(:perform_async).with('lfs', failed_registry.file_id)
subject.perform subject.perform
end end
...@@ -143,27 +143,27 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do ...@@ -143,27 +143,27 @@ describe Geo::FileDownloadDispatchWorker, :geo, :truncate do
secondary.update_attribute(:namespaces, [synced_group]) secondary.update_attribute(:namespaces, [synced_group])
end end
it 'does not perform GeoFileDownloadWorker for LFS object that does not belong to selected namespaces to replicate' do it 'does not perform Geo::FileDownloadWorker for LFS object that does not belong to selected namespaces to replicate' do
lfs_objec_in_synced_group = create(:lfs_objects_project, project: project_in_synced_group) lfs_objec_in_synced_group = create(:lfs_objects_project, project: project_in_synced_group)
create(:lfs_objects_project, project: unsynced_project) create(:lfs_objects_project, project: unsynced_project)
expect(GeoFileDownloadWorker).to receive(:perform_async) expect(Geo::FileDownloadWorker).to receive(:perform_async)
.with(:lfs, lfs_objec_in_synced_group.lfs_object_id).once.and_return(spy) .with(:lfs, lfs_objec_in_synced_group.lfs_object_id).once.and_return(spy)
subject.perform subject.perform
end end
it 'does not perform GeoFileDownloadWorker for upload objects that do not belong to selected namespaces to replicate' do it 'does not perform Geo::FileDownloadWorker for upload objects that do not belong to selected namespaces to replicate' do
avatar = fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) avatar = fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'))
avatar_in_synced_group = create(:upload, model: synced_group, path: avatar) avatar_in_synced_group = create(:upload, model: synced_group, path: avatar)
create(:upload, model: create(:group), path: avatar) create(:upload, model: create(:group), path: avatar)
avatar_in_project_in_synced_group = create(:upload, model: project_in_synced_group, path: avatar) avatar_in_project_in_synced_group = create(:upload, model: project_in_synced_group, path: avatar)
create(:upload, model: unsynced_project, path: avatar) create(:upload, model: unsynced_project, path: avatar)
expect(GeoFileDownloadWorker).to receive(:perform_async) expect(Geo::FileDownloadWorker).to receive(:perform_async)
.with('avatar', avatar_in_project_in_synced_group.id).once.and_return(spy) .with('avatar', avatar_in_project_in_synced_group.id).once.and_return(spy)
expect(GeoFileDownloadWorker).to receive(:perform_async) expect(Geo::FileDownloadWorker).to receive(:perform_async)
.with('avatar', avatar_in_synced_group.id).once.and_return(spy) .with('avatar', avatar_in_synced_group.id).once.and_return(spy)
subject.perform subject.perform
......
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