Commit 2d96e61c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent e7238677
...@@ -109,7 +109,7 @@ export default { ...@@ -109,7 +109,7 @@ export default {
<template> <template>
<p v-html="text"></p> <p v-html="text"></p>
<p v-html="confirmationTextLabel"></p> <p v-html="confirmationTextLabel"></p>
<form ref="form" :action="deleteUserUrl" method="post"> <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
<input ref="method" type="hidden" name="_method" value="delete" /> <input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" /> <input :value="csrfToken" type="hidden" name="authenticity_token" />
<gl-form-input <gl-form-input
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
%p.form-text.text-muted %p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
- if Feature.enabled?(:create_cloud_run_clusters, clusterable) - if Feature.enabled?(:create_cloud_run_clusters, clusterable, default_enabled: true)
.form-group .form-group
= provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run on GKE (beta)'), = provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run on GKE (beta)'),
label_class: 'label-bold' } label_class: 'label-bold' }
......
---
title: Fix delete user dialog bypass caused by hitting enter
merge_request: 17343
author:
type: fixed
---
title: Change commit_id type on commit_user_mentions table
merge_request: 21651
author:
type: fixed
---
title: Re-enable the cloud run feature
merge_request: https://gitlab.com/gitlab-org/gitlab/merge_requests/21762
author:
type: fixed
# frozen_string_literal: true
class ChangeCommitUserMentionsCommitIdColumnType < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
OLD_INDEX = 'commit_user_mentions_on_commit_id_and_note_id_index'
OLD_TMP_INDEX = 'temp_commit_id_and_note_id_index'
NEW_TMP_INDEX = 'temp_commit_id_for_type_change_and_note_id_index'
NEW_INDEX = 'commit_id_and_note_id_index'
def up
# the initial index name is too long and fails during migration. Renaming the index first.
add_concurrent_index :commit_user_mentions, [:commit_id, :note_id], name: OLD_TMP_INDEX
remove_concurrent_index_by_name :commit_user_mentions, OLD_INDEX
change_column_type_concurrently :commit_user_mentions, :commit_id, :string
# change_column_type_concurrently creates a new index for new column `commit_id_for_type` based on existing
# `temp_commit_id_and_note_id_index` naming it `temp_commit_id_for_type_change_and_note_id_index`, yet keeping
# `temp_commit_id_and_note_id_index` for `commit_id`, that will be cleaned
# by `cleanup_concurrent_column_type_change :commit_user_mentions, :commit_id` in a later migration.
#
# So we'll rename `temp_commit_id_for_type_change_and_note_id_index` to initialy intended name: `commit_id_and_note_id_index`.
add_concurrent_index :commit_user_mentions, [:commit_id_for_type_change, :note_id], name: NEW_INDEX
remove_concurrent_index_by_name :commit_user_mentions, NEW_TMP_INDEX
end
def down
cleanup_concurrent_column_type_change :commit_user_mentions, :commit_id
end
end
# frozen_string_literal: true
class ChangeCommitUserMentionsCommitIdColumnTypeCleanup < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
NEW_INDEX = 'commit_id_for_type_change_and_note_id_index'
OLD_INDEX = 'commit_user_mentions_on_commit_id_and_note_id_index'
def up
cleanup_concurrent_column_type_change :commit_user_mentions, :commit_id
end
def down
change_column_type_concurrently :commit_user_mentions, :commit_id, :binary
# change_column_type_concurrently creates a new index based on existing commit_id_and_note_id_index` naming it
# `commit_id_for_type_change_and_note_id_index` so we'll rename it back to its original name.
add_concurrent_index :commit_user_mentions, [:commit_id_for_type_change, :note_id], name: OLD_INDEX
remove_concurrent_index_by_name :commit_user_mentions, NEW_INDEX
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_12_17_160632) do ActiveRecord::Schema.define(version: 2019_12_16_183532) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -1217,11 +1217,11 @@ ActiveRecord::Schema.define(version: 2019_12_17_160632) do ...@@ -1217,11 +1217,11 @@ ActiveRecord::Schema.define(version: 2019_12_17_160632) do
create_table "commit_user_mentions", force: :cascade do |t| create_table "commit_user_mentions", force: :cascade do |t|
t.integer "note_id", null: false t.integer "note_id", null: false
t.binary "commit_id", null: false
t.integer "mentioned_users_ids", array: true t.integer "mentioned_users_ids", array: true
t.integer "mentioned_projects_ids", array: true t.integer "mentioned_projects_ids", array: true
t.integer "mentioned_groups_ids", array: true t.integer "mentioned_groups_ids", array: true
t.index ["commit_id", "note_id"], name: "commit_user_mentions_on_commit_id_and_note_id_index" t.string "commit_id", null: false
t.index ["commit_id", "note_id"], name: "commit_id_and_note_id_index"
t.index ["note_id"], name: "index_commit_user_mentions_on_note_id", unique: true t.index ["note_id"], name: "index_commit_user_mentions_on_note_id", unique: true
end end
......
...@@ -66,7 +66,7 @@ We need to manage the following secrets and make them match across hosts: ...@@ -66,7 +66,7 @@ We need to manage the following secrets and make them match across hosts:
#### Praefect #### Praefect
On the Praefect node we disable all other services, including Gitaly. We list each On the Praefect node we disable all other services, including Gitaly. We list each
Gitaly node that will be connected to Praefect under `praefect['storage_nodes']`. Gitaly node that will be connected to Praefect as members of the `praefect` hash in `praefect['virtual_storages']`.
In the example below, the Gitaly nodes are named `gitaly-N`. Note that one In the example below, the Gitaly nodes are named `gitaly-N`. Note that one
node is designated as primary by setting the primary to `true`. node is designated as primary by setting the primary to `true`.
...@@ -84,15 +84,6 @@ unicorn['enable'] = false ...@@ -84,15 +84,6 @@ unicorn['enable'] = false
sidekiq['enable'] = false sidekiq['enable'] = false
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
gitaly['enable'] = false gitaly['enable'] = false
```
##### Set up Praefect and its Gitaly nodes
In the example below, the Gitaly nodes are named `gitaly-X`. Note that one node is designated as
primary, by setting the primary to `true`:
```ruby
# /etc/gitlab/gitlab.rb on praefect server
# Prevent database connections during 'gitlab-ctl reconfigure' # Prevent database connections during 'gitlab-ctl reconfigure'
gitlab_rails['rake_cache_clear'] = false gitlab_rails['rake_cache_clear'] = false
...@@ -104,27 +95,27 @@ praefect['enable'] = true ...@@ -104,27 +95,27 @@ praefect['enable'] = true
# firewalls to restrict access to this address/port. # firewalls to restrict access to this address/port.
praefect['listen_addr'] = '0.0.0.0:2305' praefect['listen_addr'] = '0.0.0.0:2305'
# virtual_storage_name must match the same storage name given to praefect in git_data_dirs
praefect['virtual_storage_name'] = 'praefect'
# Replace PRAEFECT_EXTERNAL_TOKEN with a real secret # Replace PRAEFECT_EXTERNAL_TOKEN with a real secret
praefect['auth_token'] = 'PRAEFECT_EXTERNAL_TOKEN' praefect['auth_token'] = 'PRAEFECT_EXTERNAL_TOKEN'
# Replace each instance of PRAEFECT_INTERNAL_TOKEN below with a real # Replace each instance of PRAEFECT_INTERNAL_TOKEN below with a real
# secret, distinct from PRAEFECT_EXTERNAL_TOKEN. # secret, distinct from PRAEFECT_EXTERNAL_TOKEN.
praefect['storage_nodes'] = { # Name of storage hash must match storage name in git_data_dirs on GitLab server.
'gitaly-1' => { praefect['virtual_storages'] = {
'address' => 'tcp://gitaly-1.internal:8075', 'praefect' => {
'token' => 'PRAEFECT_INTERNAL_TOKEN', 'gitaly-1' => {
'primary' => true 'address' => 'tcp://gitaly-1.internal:8075',
}, 'token' => 'PRAEFECT_INTERNAL_TOKEN',
'gitaly-2' => { 'primary' => true
'address' => 'tcp://gitaly-2.internal:8075', },
'token' => 'PRAEFECT_INTERNAL_TOKEN' 'gitaly-2' => {
}, 'address' => 'tcp://gitaly-2.internal:8075',
'gitaly-3' => { 'token' => 'PRAEFECT_INTERNAL_TOKEN'
'address' => 'tcp://gitaly-3.internal:8075', },
'token' => 'PRAEFECT_INTERNAL_TOKEN' 'gitaly-3' => {
'address' => 'tcp://gitaly-3.internal:8075',
'token' => 'PRAEFECT_INTERNAL_TOKEN'
}
} }
} }
``` ```
...@@ -140,7 +131,7 @@ auth tokens from Praefect instead of GitLab. ...@@ -140,7 +131,7 @@ auth tokens from Praefect instead of GitLab.
Below is an example configuration for `gitaly-1`, the only difference for the Below is an example configuration for `gitaly-1`, the only difference for the
other Gitaly nodes is the storage name under `git_data_dirs`. other Gitaly nodes is the storage name under `git_data_dirs`.
Note that `gitaly['auth_token']` matches the `token` value listed under `praefect['storage_nodes']` Note that `gitaly['auth_token']` matches the `token` value listed under `praefect['virtual_storages']`
on the Praefect node. on the Praefect node.
```ruby ```ruby
...@@ -155,6 +146,7 @@ grafana['enable'] = false ...@@ -155,6 +146,7 @@ grafana['enable'] = false
unicorn['enable'] = false unicorn['enable'] = false
sidekiq['enable'] = false sidekiq['enable'] = false
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
prometheus_monitoring['enable'] = false
# Prevent database connections during 'gitlab-ctl reconfigure' # Prevent database connections during 'gitlab-ctl reconfigure'
gitlab_rails['rake_cache_clear'] = false gitlab_rails['rake_cache_clear'] = false
...@@ -197,7 +189,7 @@ is present, there should be two storages available to GitLab: ...@@ -197,7 +189,7 @@ is present, there should be two storages available to GitLab:
# Replace PRAEFECT_EXTERNAL_TOKEN below with real secret. # Replace PRAEFECT_EXTERNAL_TOKEN below with real secret.
git_data_dirs({ git_data_dirs({
"default" => { "default" => {
"gitaly_address" => "tcp://gitaly.internal" "path" => "/var/opt/gitlab/git-data"
}, },
"praefect" => { "praefect" => {
"gitaly_address" => "tcp://praefect.internal:2305", "gitaly_address" => "tcp://praefect.internal:2305",
...@@ -212,7 +204,9 @@ gitlab_shell['secret_token'] = 'GITLAB_SHELL_SECRET_TOKEN' ...@@ -212,7 +204,9 @@ gitlab_shell['secret_token'] = 'GITLAB_SHELL_SECRET_TOKEN'
Note that the storage name used is the same as the `praefect['virtual_storage_name']` set Note that the storage name used is the same as the `praefect['virtual_storage_name']` set
on the Praefect node. on the Praefect node.
Restart GitLab using `gitlab-ctl restart` on the GitLab node. Save your changes and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
Run `gitlab-rake gitlab:gitaly:check` to confirm that GitLab can reach Praefect.
### Testing Praefect ### Testing Praefect
...@@ -220,6 +214,18 @@ To test Praefect, first set it as the default storage node for new projects ...@@ -220,6 +214,18 @@ To test Praefect, first set it as the default storage node for new projects
using **Admin Area > Settings > Repository > Repository storage**. Next, using **Admin Area > Settings > Repository > Repository storage**. Next,
create a new project and check the "Initialize repository with a README" box. create a new project and check the "Initialize repository with a README" box.
If you receive a 503 error, check `/var/log/gitlab/gitlab-rails/production.log`. If you receive an error, check `/var/log/gitlab/gitlab-rails/production.log`.
A `GRPC::Unavailable (14:failed to connect to all addresses)` error indicates
that GitLab was unable to connect to Praefect. Here are common errors and potential causes:
- 500 response code
- **ActionView::Template::Error (7:permission denied)**
- `praefect['auth_token']` and `gitlab_rails['gitaly_token']` do not match on the GitLab server.
- **Unable to save project. Error: 7:permission denied**
- Secret token in `praefect['storage_nodes']` on GitLab server does not match the
value in `gitaly['auth_token']` on one or more Gitaly servers.
- 503 response code
- **GRPC::Unavailable (14:failed to connect to all addresses)**
- GitLab was unable to reach Praefect.
- **GRPC::Unavailable (14:all SubCons are in TransientFailure...)**
- Praefect cannot reach one or more of its child Gitaly nodes.
...@@ -57,7 +57,7 @@ Parameters: ...@@ -57,7 +57,7 @@ Parameters:
Creates a two-way relation between two issues. User must be allowed to update both issues in order to succeed. Creates a two-way relation between two issues. User must be allowed to update both issues in order to succeed.
``` ```
POST /projects/:id/issues/:issue_iid/links POST /projects/:id/issues/:issue_iid/links/:target_project_id/:target_issue_iid
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
......
...@@ -3,26 +3,46 @@ import { GlButton, GlFormInput } from '@gitlab/ui'; ...@@ -3,26 +3,46 @@ import { GlButton, GlFormInput } from '@gitlab/ui';
import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue'; import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
import ModalStub from './stubs/modal_stub'; import ModalStub from './stubs/modal_stub';
const TEST_DELETE_USER_URL = 'delete-url';
const TEST_BLOCK_USER_URL = 'block-url';
const TEST_CSRF = 'csrf';
describe('User Operation confirmation modal', () => { describe('User Operation confirmation modal', () => {
let wrapper; let wrapper;
let formSubmitSpy;
const findButton = variant => const findButton = variant =>
wrapper wrapper
.findAll(GlButton) .findAll(GlButton)
.filter(w => w.attributes('variant') === variant) .filter(w => w.attributes('variant') === variant)
.at(0); .at(0);
const findForm = () => wrapper.find('form');
const findUsernameInput = () => wrapper.find(GlFormInput);
const findPrimaryButton = () => findButton('danger');
const findSecondaryButton = () => findButton('warning');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
const getUsername = () => findUsernameInput().attributes('value');
const getMethodParam = () => new FormData(findForm().element).get('_method');
const getFormAction = () => findForm().attributes('action');
const setUsername = username => {
findUsernameInput().vm.$emit('input', username);
};
const username = 'username';
const badUsername = 'bad_username';
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteUserModal, { wrapper = shallowMount(DeleteUserModal, {
propsData: { propsData: {
username,
title: 'title', title: 'title',
content: 'content', content: 'content',
action: 'action', action: 'action',
secondaryAction: 'secondaryAction', secondaryAction: 'secondaryAction',
deleteUserUrl: 'delete-url', deleteUserUrl: TEST_DELETE_USER_URL,
blockUserUrl: 'block-url', blockUserUrl: TEST_BLOCK_USER_URL,
username: 'username', csrfToken: TEST_CSRF,
csrfToken: 'csrf',
...props, ...props,
}, },
stubs: { stubs: {
...@@ -32,6 +52,10 @@ describe('User Operation confirmation modal', () => { ...@@ -32,6 +52,10 @@ describe('User Operation confirmation modal', () => {
}); });
}; };
beforeEach(() => {
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -42,44 +66,84 @@ describe('User Operation confirmation modal', () => { ...@@ -42,44 +66,84 @@ describe('User Operation confirmation modal', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it.each` describe('on created', () => {
variant | prop | action beforeEach(() => {
${'danger'} | ${'deleteUserUrl'} | ${'delete'} createComponent();
${'warning'} | ${'blockUserUrl'} | ${'block'} });
`('closing modal with $variant button triggers $action', ({ variant, prop }) => {
createComponent(); it('has disabled buttons', () => {
const form = wrapper.find('form'); expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
jest.spyOn(form.element, 'submit').mockReturnValue(); expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
const modalButton = findButton(variant);
modalButton.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(form.element.submit).toHaveBeenCalled();
expect(form.element.action).toContain(wrapper.props(prop));
expect(new FormData(form.element).get('authenticity_token')).toEqual(
wrapper.props('csrfToken'),
);
}); });
}); });
it('disables buttons by default', () => { describe('with incorrect username', () => {
createComponent(); beforeEach(() => {
const blockButton = findButton('warning'); createComponent();
const deleteButton = findButton('danger'); setUsername(badUsername);
expect(blockButton.attributes().disabled).toBeTruthy();
expect(deleteButton.attributes().disabled).toBeTruthy(); return wrapper.vm.$nextTick();
});
it('shows incorrect username', () => {
expect(getUsername()).toEqual(badUsername);
});
it('has disabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
});
}); });
it('enables button when username is typed', () => { describe('with correct username', () => {
createComponent({ beforeEach(() => {
username: 'some-username', createComponent();
setUsername(username);
return wrapper.vm.$nextTick();
});
it('shows correct username', () => {
expect(getUsername()).toEqual(username);
});
it('has enabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeFalsy();
expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
}); });
wrapper.find(GlFormInput).vm.$emit('input', 'some-username');
const blockButton = findButton('warning');
const deleteButton = findButton('danger');
return wrapper.vm.$nextTick().then(() => { describe('when primary action is submitted', () => {
expect(blockButton.attributes().disabled).toBeFalsy(); beforeEach(() => {
expect(deleteButton.attributes().disabled).toBeFalsy(); findPrimaryButton().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('clears the input', () => {
expect(getUsername()).toEqual('');
});
it('has correct form attributes and calls submit', () => {
expect(getFormAction()).toBe(TEST_DELETE_USER_URL);
expect(getMethodParam()).toBe('delete');
expect(findAuthenticityToken()).toBe(TEST_CSRF);
expect(formSubmitSpy).toHaveBeenCalled();
});
});
describe('when secondary action is submitted', () => {
beforeEach(() => {
findSecondaryButton().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('has correct form attributes and calls submit', () => {
expect(getFormAction()).toBe(TEST_BLOCK_USER_URL);
expect(getMethodParam()).toBe('put');
expect(findAuthenticityToken()).toBe(TEST_CSRF);
expect(formSubmitSpy).toHaveBeenCalled();
});
}); });
}); });
}); });
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