Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6b833f1e03
commit
ca05512007
|
@ -82,9 +82,9 @@ GitLab is a Ruby on Rails application that runs on the following software:
|
|||
- Ruby (MRI) 2.6.5
|
||||
- Git 2.8.4+
|
||||
- Redis 2.8+
|
||||
- PostgreSQL (preferred) or MySQL
|
||||
- PostgreSQL 9.6+
|
||||
|
||||
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
|
||||
For more information please see the [architecture](https://docs.gitlab.com/ee/development/architecture.html) and [requirements](https://docs.gitlab.com/ee/install/requirements.html) documentation.
|
||||
|
||||
## UX design
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<script>
|
||||
import { GlButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
ModalCopyButton,
|
||||
},
|
||||
directives: {
|
||||
'gl-modal': GlModalDirective,
|
||||
},
|
||||
instructionText: {
|
||||
step1: s__(
|
||||
'EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}.',
|
||||
),
|
||||
step2: s__('EnableReviewApp|%{stepStart}Step 2%{stepEnd}. Copy the following snippet:'),
|
||||
step3: s__(
|
||||
`EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file.`,
|
||||
),
|
||||
},
|
||||
modalInfo: {
|
||||
closeText: s__('EnableReviewApp|Close'),
|
||||
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
|
||||
copyString: `deploy_review
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Deploy a review app"
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
||||
only: branches
|
||||
except: master`,
|
||||
id: 'enable-review-app-info',
|
||||
title: s__('ReviewApp|Enable Review App'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-button
|
||||
v-gl-modal="$options.modalInfo.id"
|
||||
variant="info"
|
||||
category="secondary"
|
||||
type="button"
|
||||
class="js-enable-review-app-button"
|
||||
>
|
||||
{{ s__('Environments|Enable review app') }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
:modal-id="$options.modalInfo.id"
|
||||
:title="$options.modalInfo.title"
|
||||
size="lg"
|
||||
class="text-2 ws-normal"
|
||||
ok-only
|
||||
ok-variant="light"
|
||||
:ok-title="$options.modalInfo.closeText"
|
||||
>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.instructionText.step1">
|
||||
<template #step="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #link="{ content }">
|
||||
<gl-link
|
||||
href="https://docs.gitlab.com/ee/user/project/clusters/add_remove_clusters.html"
|
||||
target="_blank"
|
||||
>{{ content }}</gl-link
|
||||
>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.instructionText.step2">
|
||||
<template #step="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<div class="flex align-items-start">
|
||||
<pre class="w-100"> {{ $options.modalInfo.copyString }} </pre>
|
||||
<modal-copy-button
|
||||
:title="$options.modalInfo.copyToClipboardText"
|
||||
:text="$options.modalInfo.copyString"
|
||||
:modal-id="$options.modalInfo.id"
|
||||
css-classes="border-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.instructionText.step3">
|
||||
<template #step="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #link="{ content }">
|
||||
<gl-link href="blob/master/.gitlab-ci.yml" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
|
@ -1,19 +1,23 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import envrionmentsAppMixin from 'ee_else_ce/environments/mixins/environments_app_mixin';
|
||||
import Flash from '../../flash';
|
||||
import { s__ } from '../../locale';
|
||||
import Flash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import emptyState from './empty_state.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import environmentsMixin from '../mixins/environments_mixin';
|
||||
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import EnableReviewAppButton from './enable_review_app_button.vue';
|
||||
import StopEnvironmentModal from './stop_environment_modal.vue';
|
||||
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
emptyState,
|
||||
StopEnvironmentModal,
|
||||
ConfirmRollbackModal,
|
||||
emptyState,
|
||||
EnableReviewAppButton,
|
||||
GlButton,
|
||||
StopEnvironmentModal,
|
||||
},
|
||||
|
||||
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
|
||||
|
@ -96,10 +100,16 @@ export default {
|
|||
<div class="top-area">
|
||||
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
|
||||
|
||||
<div v-if="canCreateEnvironment && !isLoading" class="nav-controls">
|
||||
<a :href="newEnvironmentPath" class="btn btn-success">
|
||||
<div class="nav-controls">
|
||||
<enable-review-app-button v-if="state.reviewAppDetails.can_setup_review_app" class="mr-2" />
|
||||
<gl-button
|
||||
v-if="canCreateEnvironment && !isLoading"
|
||||
:href="newEnvironmentPath"
|
||||
category="primary"
|
||||
variant="success"
|
||||
>
|
||||
{{ s__('Environments|New environment') }}
|
||||
</a>
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ export default {
|
|||
this.store.storeAvailableCount(resp.data.available_count);
|
||||
this.store.storeStoppedCount(resp.data.stopped_count);
|
||||
this.store.storeEnvironments(resp.data.environments);
|
||||
this.store.setReviewAppDetails(resp.data.review_app);
|
||||
this.store.setPagination(resp.headers);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ export default class EnvironmentsStore {
|
|||
this.state.stoppedCounter = 0;
|
||||
this.state.availableCounter = 0;
|
||||
this.state.paginationInformation = {};
|
||||
this.state.reviewAppDetails = {};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -104,6 +105,11 @@ export default class EnvironmentsStore {
|
|||
return paginationInformation;
|
||||
}
|
||||
|
||||
setReviewAppDetails(details = {}) {
|
||||
this.state.reviewAppDetails = details;
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the number of available environments.
|
||||
*
|
||||
|
|
|
@ -37,7 +37,6 @@ class ApplicationController < ActionController::Base
|
|||
around_action :set_current_context
|
||||
around_action :set_locale
|
||||
around_action :set_session_storage
|
||||
around_action :set_current_admin
|
||||
|
||||
after_action :set_page_title_header, if: :json_request?
|
||||
after_action :limit_session_time, if: -> { !current_user }
|
||||
|
@ -474,13 +473,6 @@ class ApplicationController < ActionController::Base
|
|||
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
|
||||
end
|
||||
|
||||
def set_current_admin(&block)
|
||||
return yield unless Feature.enabled?(:user_mode_in_session)
|
||||
return yield unless current_user
|
||||
|
||||
Gitlab::Auth::CurrentUserMode.with_current_admin(current_user, &block)
|
||||
end
|
||||
|
||||
def html_request?
|
||||
request.format.html?
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Create conditional Enable Review App button
|
||||
merge_request: 23703
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add cycle analytics duration chart with median line
|
||||
merge_request: 23971
|
||||
author:
|
||||
type: added
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Admin mode support in sidekiq jobs
|
||||
merge_request: 21792
|
||||
author: Diego Louzán
|
||||
type: changed
|
|
@ -347,6 +347,12 @@ reviewee.
|
|||
of the contributed code. It's usually a good idea to ask another maintainer or
|
||||
reviewer before doing it, but have the courage to do it when you believe it is
|
||||
important.
|
||||
- In the interest of [Iteration](https://about.gitlab.com/handbook/values/#iteration),
|
||||
if, as a reviewer, your suggestions are non-blocking changes or personal preference
|
||||
(not a documented or agreed requirement), consider approving the merge request
|
||||
before passing it back to the author. This allows them to implement your suggestions
|
||||
if they agree, or allows them to pass it onto the
|
||||
maintainer for review straight away. This can help reduce our overall time-to-merge.
|
||||
- There is a difference in doing things right and doing things right now.
|
||||
Ideally, we should do the former, but in the real world we need the latter as
|
||||
well. A good example is a security fix which should be released as soon as
|
||||
|
|
|
@ -379,10 +379,6 @@ Rails migration example:
|
|||
|
||||
```ruby
|
||||
add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
|
||||
|
||||
# or
|
||||
|
||||
add_column(:projects, :foo, :integer, default: 10, limit: 8)
|
||||
```
|
||||
|
||||
## Timestamp column type
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
# `ReactiveCaching`
|
||||
|
||||
> This doc refers to <https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/concerns/reactive_caching.rb>.
|
||||
|
||||
The `ReactiveCaching` concern is used for fetching some data in the background and store it
|
||||
in the Rails cache, keeping it up-to-date for as long as it is being requested. If the
|
||||
data hasn't been requested for `reactive_cache_lifetime`, it will stop being refreshed,
|
||||
and then be removed.
|
||||
|
||||
## Examples
|
||||
|
||||
```ruby
|
||||
class Foo < ApplicationRecord
|
||||
include ReactiveCaching
|
||||
|
||||
after_save :clear_reactive_cache!
|
||||
|
||||
def calculate_reactive_cache(param1, param2)
|
||||
# Expensive operation here. The return value of this method is cached
|
||||
end
|
||||
|
||||
def result
|
||||
# Any arguments can be passed to `with_reactive_cache`. `calculate_reactive_cache`
|
||||
# will be called with the same arguments.
|
||||
with_reactive_cache(param1, param2) do |data|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In this example, the first time `#result` is called, it will return `nil`. However,
|
||||
it will enqueue a background worker to call `#calculate_reactive_cache` and set an
|
||||
initial cache lifetime of 10 min.
|
||||
|
||||
## How it works
|
||||
|
||||
The first time `#with_reactive_cache` is called, a background job is enqueued and
|
||||
`with_reactive_cache` returns `nil`. The background job calls `#calculate_reactive_cache`
|
||||
and stores its return value. It also re-enqueues the background job to run again after
|
||||
`reactive_cache_refresh_interval`. Therefore, it will keep the stored value up to date.
|
||||
Calculations never run concurrently.
|
||||
|
||||
Calling `#with_reactive_cache` while a value is cached will call the block given to
|
||||
`#with_reactive_cache`, yielding the cached value. It will also extend the lifetime
|
||||
of the cache by the `reactive_cache_lifetime` value.
|
||||
|
||||
Once the lifetime has expired, no more background jobs will be enqueued and calling
|
||||
`#with_reactive_cache` will again return `nil` - starting the process all over again.
|
||||
|
||||
## When to use
|
||||
|
||||
- If we need to make a request to an external API (for example, requests to the k8s API).
|
||||
It is not advisable to keep the application server worker blocked for the duration of
|
||||
the external request.
|
||||
- If a model needs to perform a lot of database calls or other time consuming
|
||||
calculations.
|
||||
|
||||
## How to use
|
||||
|
||||
### In models and services
|
||||
|
||||
The ReactiveCaching concern can be used in models as well as `project_services`
|
||||
(`app/models/project_services`).
|
||||
|
||||
1. Include the concern in your model or service.
|
||||
|
||||
When including in a model:
|
||||
|
||||
```ruby
|
||||
include ReactiveCaching
|
||||
```
|
||||
|
||||
or when including in a `project_service`:
|
||||
|
||||
```ruby
|
||||
include ReactiveService
|
||||
```
|
||||
|
||||
1. Implement the `calculate_reactive_cache` method in your model/service.
|
||||
1. Call `with_reactive_cache` in your model/service where the cached value is needed.
|
||||
|
||||
### In controllers
|
||||
|
||||
Controller endpoints that call a model or service method that uses `ReactiveCaching` should
|
||||
not wait until the background worker completes.
|
||||
|
||||
- An API that calls a model or service method that uses `ReactiveCaching` should return
|
||||
`202 accepted` when the cache is being calculated (when `#with_reactive_cache` returns `nil`).
|
||||
- It should also
|
||||
[set the polling interval header](fe_guide/performance.md#realtime-components) with
|
||||
`Gitlab::PollingInterval.set_header`.
|
||||
- The consumer of the API is expected to poll the API.
|
||||
- You can also consider implementing [ETag caching](polling.md) to reduce the server
|
||||
load caused by polling.
|
||||
|
||||
### Methods to implement in a model or service
|
||||
|
||||
These are methods that should be implemented in the model/service that includes `ReactiveCaching`.
|
||||
|
||||
#### `#calculate_reactive_cache` (required)
|
||||
|
||||
- This method must be implemented. Its return value will be cached.
|
||||
- It will be called by `ReactiveCaching` when it needs to populate the cache.
|
||||
- Any arguments passed to `with_reactive_cache` will also be passed to `calculate_reactive_cache`.
|
||||
|
||||
#### `#reactive_cache_updated` (optional)
|
||||
|
||||
- This method can be implemented if needed.
|
||||
- It is called by the `ReactiveCaching` concern whenever the cache is updated.
|
||||
If the cache is being refreshed and the new cache value is the same as the old cache
|
||||
value, this method will not be called. It is only called if a new value is stored in
|
||||
the cache.
|
||||
- It can be used to perform an action whenever the cache is updated.
|
||||
|
||||
### Methods called by a model or service
|
||||
|
||||
These are methods provided by `ReactiveCaching` and should be called in
|
||||
the model/service.
|
||||
|
||||
#### `#with_reactive_cache` (required)
|
||||
|
||||
- `with_reactive_cache` must be called where the result of `calculate_reactive_cache`
|
||||
is required.
|
||||
- A block can be given to `with_reactive_cache`. `with_reactive_cache` can also take
|
||||
any number of arguments. Any arguments passed to `with_reactive_cache` will be
|
||||
passed to `calculate_reactive_cache`. The arguments passed to `with_reactive_cache`
|
||||
will be appended to the cache key name.
|
||||
- If `with_reactive_cache` is called when the result has already been cached, the
|
||||
block will be called, yielding the cached value and the return value of the block
|
||||
will be returned by `with_reactive_cache`. It will also reset the timeout of the
|
||||
cache to the `reactive_cache_lifetime` value.
|
||||
- If the result has not been cached as yet, `with_reactive_cache` will return nil.
|
||||
It will also enqueue a background job, which will call `calculate_reactive_cache`
|
||||
and cache the result.
|
||||
- Once the background job has completed and the result is cached, the next call
|
||||
to `with_reactive_cache` will pick up the cached value.
|
||||
- In the example below, `data` is the cached value which is yielded to the block
|
||||
given to `with_reactive_cache`.
|
||||
|
||||
```ruby
|
||||
class Foo < ApplicationRecord
|
||||
include ReactiveCaching
|
||||
|
||||
def calculate_reactive_cache(param1, param2)
|
||||
# Expensive operation here. The return value of this method is cached
|
||||
end
|
||||
|
||||
def result
|
||||
with_reactive_cache(param1, param2) do |data|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### `#clear_reactive_cache!` (optional)
|
||||
|
||||
- This method can be called when the cache needs to be expired/cleared. For example,
|
||||
it can be called in an `after_save` callback in a model so that the cache is
|
||||
cleared after the model is modified.
|
||||
- This method should be called with the same parameters that are passed to
|
||||
`with_reactive_cache` because the parameters are part of the cache key.
|
||||
|
||||
#### `#without_reactive_cache` (optional)
|
||||
|
||||
- This is a convenience method that can be used for debugging purposes.
|
||||
- This method calls `calculate_reactive_cache` in the current process instead of
|
||||
in a background worker.
|
||||
|
||||
### Configurable options
|
||||
|
||||
There are some `class_attribute` options which can be tweaked.
|
||||
|
||||
#### `self.reactive_cache_key`
|
||||
|
||||
- The value of this attribute is the prefix to the `data` and `alive` cache key names.
|
||||
The parameters passed to `with_reactive_cache` form the rest of the cache key names.
|
||||
- By default, this key uses the model's name and the ID of the record.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_key = -> (record) { [model_name.singular, record.id] }
|
||||
```
|
||||
|
||||
- The `data` and `alive` cache keys in this case will be `"ExampleModel:1:arg1:arg2"`
|
||||
and `"ExampleModel:1:arg1:arg2:alive"` respectively, where `ExampleModel` is the
|
||||
name of the model, `1` is the ID of the record, `arg1` and `arg2` are parameters
|
||||
passed to `with_reactive_cache`.
|
||||
- If you're including this concern in a service instead, you will need to override
|
||||
the default by adding the following to your service:
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
|
||||
```
|
||||
|
||||
If your reactive_cache_key is exactly like the above, you can use the existing
|
||||
`ReactiveService` concern instead.
|
||||
|
||||
#### `self.reactive_cache_lease_timeout`
|
||||
|
||||
- `ReactiveCaching` uses `Gitlab::ExclusiveLease` to ensure that the cache calculation
|
||||
is never run concurrently by multiple workers.
|
||||
- This attribute is the timeout for the `Gitlab::ExclusiveLease`.
|
||||
- It defaults to 2 minutes, but can be overriden if a different timeout is required.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_lease_timeout = 2.minutes
|
||||
```
|
||||
|
||||
#### `self.reactive_cache_refresh_interval`
|
||||
|
||||
- This is the interval at which the cache is refreshed.
|
||||
- It defaults to 1 minute.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_lease_timeout = 1.minute
|
||||
```
|
||||
|
||||
#### `self.reactive_cache_lifetime`
|
||||
|
||||
- This is the duration after which the cache will be cleared if there are no requests.
|
||||
- The default is 10 minutes. If there are no requests for this cache value for 10 minutes,
|
||||
the cache will expire.
|
||||
- If the cache value is requested before it expires, the timeout of the cache will
|
||||
be reset to `reactive_cache_lifetime`.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_lifetime = 10.minutes
|
||||
```
|
||||
|
||||
#### `self.reactive_cache_worker_finder`
|
||||
|
||||
- This is the method used by the background worker to find or generate the object on
|
||||
which `calculate_reactive_cache` can be called.
|
||||
- By default it uses the model primary key to find the object:
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_worker_finder = ->(id, *_args) do
|
||||
find_by(primary_key => id)
|
||||
end
|
||||
```
|
||||
|
||||
- The default behaviour can be overridden by defining a custom `reactive_cache_worker_finder`.
|
||||
|
||||
```ruby
|
||||
class Foo < ApplicationRecord
|
||||
include ReactiveCaching
|
||||
|
||||
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
|
||||
|
||||
def self.from_cache(var1, var2)
|
||||
# This method will be called by the background worker with "bar1" and
|
||||
# "bar2" as arguments.
|
||||
new(var1, var2)
|
||||
end
|
||||
|
||||
def initialize(var1, var2)
|
||||
# ...
|
||||
end
|
||||
|
||||
def calculate_reactive_cache(var1, var2)
|
||||
# Expensive operation here. The return value of this method is cached
|
||||
end
|
||||
|
||||
def result
|
||||
with_reactive_cache("bar1", "bar2") do |data|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- In this example, the primary key ID will be passed to `reactive_cache_worker_finder`
|
||||
along with the parameters passed to `with_reactive_cache`.
|
||||
- The custom `reactive_cache_worker_finder` calls `.from_cache` with the parameters
|
||||
passed to `with_reactive_cache`.
|
|
@ -196,277 +196,4 @@ end
|
|||
|
||||
## `ReactiveCaching`
|
||||
|
||||
> This doc refers to <https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/concerns/reactive_caching.rb>.
|
||||
|
||||
The `ReactiveCaching` concern is used for fetching some data in the background and store it
|
||||
in the Rails cache, keeping it up-to-date for as long as it is being requested. If the
|
||||
data hasn't been requested for `reactive_cache_lifetime`, it will stop being refreshed,
|
||||
and then be removed.
|
||||
|
||||
### Examples
|
||||
|
||||
```ruby
|
||||
class Foo < ApplicationRecord
|
||||
include ReactiveCaching
|
||||
|
||||
after_save :clear_reactive_cache!
|
||||
|
||||
def calculate_reactive_cache(param1, param2)
|
||||
# Expensive operation here. The return value of this method is cached
|
||||
end
|
||||
|
||||
def result
|
||||
# Any arguments can be passed to `with_reactive_cache`. `calculate_reactive_cache`
|
||||
# will be called with the same arguments.
|
||||
with_reactive_cache(param1, param2) do |data|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In this example, the first time `#result` is called, it will return `nil`. However,
|
||||
it will enqueue a background worker to call `#calculate_reactive_cache` and set an
|
||||
initial cache lifetime of 10 min.
|
||||
|
||||
### How it works
|
||||
|
||||
The first time `#with_reactive_cache` is called, a background job is enqueued and
|
||||
`with_reactive_cache` returns `nil`. The background job calls `#calculate_reactive_cache`
|
||||
and stores its return value. It also re-enqueues the background job to run again after
|
||||
`reactive_cache_refresh_interval`. Therefore, it will keep the stored value up to date.
|
||||
Calculations never run concurrently.
|
||||
|
||||
Calling `#with_reactive_cache` while a value is cached will call the block given to
|
||||
`#with_reactive_cache`, yielding the cached value. It will also extend the lifetime
|
||||
of the cache by the `reactive_cache_lifetime` value.
|
||||
|
||||
Once the lifetime has expired, no more background jobs will be enqueued and calling
|
||||
`#with_reactive_cache` will again return `nil` - starting the process all over again.
|
||||
|
||||
### When to use
|
||||
|
||||
- If we need to make a request to an external API (for example, requests to the k8s API).
|
||||
It is not advisable to keep the application server worker blocked for the duration of
|
||||
the external request.
|
||||
- If a model needs to perform a lot of database calls or other time consuming
|
||||
calculations.
|
||||
|
||||
### How to use
|
||||
|
||||
#### In models and services
|
||||
|
||||
The ReactiveCaching concern can be used in models as well as `project_services`
|
||||
(`app/models/project_services`).
|
||||
|
||||
1. Include the concern in your model or service.
|
||||
|
||||
When including in a model:
|
||||
|
||||
```ruby
|
||||
include ReactiveCaching
|
||||
```
|
||||
|
||||
or when including in a `project_service`:
|
||||
|
||||
```ruby
|
||||
include ReactiveService
|
||||
```
|
||||
|
||||
1. Implement the `calculate_reactive_cache` method in your model/service.
|
||||
1. Call `with_reactive_cache` in your model/service where the cached value is needed.
|
||||
|
||||
#### In controllers
|
||||
|
||||
Controller endpoints that call a model or service method that uses `ReactiveCaching` should
|
||||
not wait until the background worker completes.
|
||||
|
||||
- An API that calls a model or service method that uses `ReactiveCaching` should return
|
||||
`202 accepted` when the cache is being calculated (when `#with_reactive_cache` returns `nil`).
|
||||
- It should also
|
||||
[set the polling interval header](fe_guide/performance.md#realtime-components) with
|
||||
`Gitlab::PollingInterval.set_header`.
|
||||
- The consumer of the API is expected to poll the API.
|
||||
- You can also consider implementing [ETag caching](polling.md) to reduce the server
|
||||
load caused by polling.
|
||||
|
||||
#### Methods to implement in a model or service
|
||||
|
||||
These are methods that should be implemented in the model/service that includes `ReactiveCaching`.
|
||||
|
||||
##### `#calculate_reactive_cache` (required)
|
||||
|
||||
- This method must be implemented. Its return value will be cached.
|
||||
- It will be called by `ReactiveCaching` when it needs to populate the cache.
|
||||
- Any arguments passed to `with_reactive_cache` will also be passed to `calculate_reactive_cache`.
|
||||
|
||||
##### `#reactive_cache_updated` (optional)
|
||||
|
||||
- This method can be implemented if needed.
|
||||
- It is called by the `ReactiveCaching` concern whenever the cache is updated.
|
||||
If the cache is being refreshed and the new cache value is the same as the old cache
|
||||
value, this method will not be called. It is only called if a new value is stored in
|
||||
the cache.
|
||||
- It can be used to perform an action whenever the cache is updated.
|
||||
|
||||
#### Methods called by a model or service
|
||||
|
||||
These are methods provided by `ReactiveCaching` and should be called in
|
||||
the model/service.
|
||||
|
||||
##### `#with_reactive_cache` (required)
|
||||
|
||||
- `with_reactive_cache` must be called where the result of `calculate_reactive_cache`
|
||||
is required.
|
||||
- A block can be given to `with_reactive_cache`. `with_reactive_cache` can also take
|
||||
any number of arguments. Any arguments passed to `with_reactive_cache` will be
|
||||
passed to `calculate_reactive_cache`. The arguments passed to `with_reactive_cache`
|
||||
will be appended to the cache key name.
|
||||
- If `with_reactive_cache` is called when the result has already been cached, the
|
||||
block will be called, yielding the cached value and the return value of the block
|
||||
will be returned by `with_reactive_cache`. It will also reset the timeout of the
|
||||
cache to the `reactive_cache_lifetime` value.
|
||||
- If the result has not been cached as yet, `with_reactive_cache` will return nil.
|
||||
It will also enqueue a background job, which will call `calculate_reactive_cache`
|
||||
and cache the result.
|
||||
- Once the background job has completed and the result is cached, the next call
|
||||
to `with_reactive_cache` will pick up the cached value.
|
||||
- In the example below, `data` is the cached value which is yielded to the block
|
||||
given to `with_reactive_cache`.
|
||||
|
||||
```ruby
|
||||
class Foo < ApplicationRecord
|
||||
include ReactiveCaching
|
||||
|
||||
def calculate_reactive_cache(param1, param2)
|
||||
# Expensive operation here. The return value of this method is cached
|
||||
end
|
||||
|
||||
def result
|
||||
with_reactive_cache(param1, param2) do |data|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
##### `#clear_reactive_cache!` (optional)
|
||||
|
||||
- This method can be called when the cache needs to be expired/cleared. For example,
|
||||
it can be called in an `after_save` callback in a model so that the cache is
|
||||
cleared after the model is modified.
|
||||
- This method should be called with the same parameters that are passed to
|
||||
`with_reactive_cache` because the parameters are part of the cache key.
|
||||
|
||||
##### `#without_reactive_cache` (optional)
|
||||
|
||||
- This is a convenience method that can be used for debugging purposes.
|
||||
- This method calls `calculate_reactive_cache` in the current process instead of
|
||||
in a background worker.
|
||||
|
||||
#### Configurable options
|
||||
|
||||
There are some `class_attribute` options which can be tweaked.
|
||||
|
||||
##### `self.reactive_cache_key`
|
||||
|
||||
- The value of this attribute is the prefix to the `data` and `alive` cache key names.
|
||||
The parameters passed to `with_reactive_cache` form the rest of the cache key names.
|
||||
- By default, this key uses the model's name and the ID of the record.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_key = -> (record) { [model_name.singular, record.id] }
|
||||
```
|
||||
|
||||
- The `data` and `alive` cache keys in this case will be `"ExampleModel:1:arg1:arg2"`
|
||||
and `"ExampleModel:1:arg1:arg2:alive"` respectively, where `ExampleModel` is the
|
||||
name of the model, `1` is the ID of the record, `arg1` and `arg2` are parameters
|
||||
passed to `with_reactive_cache`.
|
||||
- If you're including this concern in a service instead, you will need to override
|
||||
the default by adding the following to your service:
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
|
||||
```
|
||||
|
||||
If your reactive_cache_key is exactly like the above, you can use the existing
|
||||
`ReactiveService` concern instead.
|
||||
|
||||
##### `self.reactive_cache_lease_timeout`
|
||||
|
||||
- `ReactiveCaching` uses `Gitlab::ExclusiveLease` to ensure that the cache calculation
|
||||
is never run concurrently by multiple workers.
|
||||
- This attribute is the timeout for the `Gitlab::ExclusiveLease`.
|
||||
- It defaults to 2 minutes, but can be overriden if a different timeout is required.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_lease_timeout = 2.minutes
|
||||
```
|
||||
|
||||
##### `self.reactive_cache_refresh_interval`
|
||||
|
||||
- This is the interval at which the cache is refreshed.
|
||||
- It defaults to 1 minute.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_lease_timeout = 1.minute
|
||||
```
|
||||
|
||||
##### `self.reactive_cache_lifetime`
|
||||
|
||||
- This is the duration after which the cache will be cleared if there are no requests.
|
||||
- The default is 10 minutes. If there are no requests for this cache value for 10 minutes,
|
||||
the cache will expire.
|
||||
- If the cache value is requested before it expires, the timeout of the cache will
|
||||
be reset to `reactive_cache_lifetime`.
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_lifetime = 10.minutes
|
||||
```
|
||||
|
||||
##### `self.reactive_cache_worker_finder`
|
||||
|
||||
- This is the method used by the background worker to find or generate the object on
|
||||
which `calculate_reactive_cache` can be called.
|
||||
- By default it uses the model primary key to find the object:
|
||||
|
||||
```ruby
|
||||
self.reactive_cache_worker_finder = ->(id, *_args) do
|
||||
find_by(primary_key => id)
|
||||
end
|
||||
```
|
||||
|
||||
- The default behaviour can be overridden by defining a custom `reactive_cache_worker_finder`.
|
||||
|
||||
```ruby
|
||||
class Foo < ApplicationRecord
|
||||
include ReactiveCaching
|
||||
|
||||
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
|
||||
|
||||
def self.from_cache(var1, var2)
|
||||
# This method will be called by the background worker with "bar1" and
|
||||
# "bar2" as arguments.
|
||||
new(var1, var2)
|
||||
end
|
||||
|
||||
def initialize(var1, var2)
|
||||
# ...
|
||||
end
|
||||
|
||||
def calculate_reactive_cache(var1, var2)
|
||||
# Expensive operation here. The return value of this method is cached
|
||||
end
|
||||
|
||||
def result
|
||||
with_reactive_cache("bar1", "bar2") do |data|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- In this example, the primary key ID will be passed to `reactive_cache_worker_finder`
|
||||
along with the parameters passed to `with_reactive_cache`.
|
||||
- The custom `reactive_cache_worker_finder` calls `.from_cache` with the parameters
|
||||
passed to `with_reactive_cache`.
|
||||
Read the documentation on [`ReactiveCaching`](reactive_caching.md).
|
||||
|
|
|
@ -172,16 +172,23 @@ For example, if 30 days worth of data has been selected (for example, 2019-12-16
|
|||
median line will represent the previous 30 days worth of data (2019-11-16 to 2019-12-16)
|
||||
as a metric to compare against.
|
||||
|
||||
### Enabling chart
|
||||
### Disabling chart
|
||||
|
||||
By default, this chart is disabled for self-managed instances. To enable it, ask an
|
||||
administrator with Rails console access to run the following:
|
||||
This chart is enabled by default. If you have a self-managed instance, an
|
||||
administrator can open a Rails console and disable it with the following command:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:cycle_analytics_scatterplot_enabled)
|
||||
Feature.disable(:cycle_analytics_scatterplot_enabled)
|
||||
```
|
||||
|
||||
This chart is enabled by default on GitLab.com.
|
||||
### Disabling chart median line
|
||||
|
||||
This chart median line is enabled by default. If you have a self-managed instance, an
|
||||
administrator can open a Rails console and disable it with the following command:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:cycle_analytics_scatterplot_median_enabled)
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
|
@ -74,9 +74,9 @@ To enable this merge request approval rule:
|
|||
|
||||
1. Navigate to your project's **Settings > General** and expand
|
||||
**Merge request approvals**.
|
||||
1. Locate **All members with Developer role or higher and code owners (if any)** and click **Edit** to choose the number of approvals required.
|
||||
1. Locate **Any eligible user** and choose the number of approvals required.
|
||||
|
||||
![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_4.png)
|
||||
![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_7.png)
|
||||
|
||||
Once set, merge requests can only be merged once approved by the
|
||||
number of approvals you've set. GitLab will accept approvals from
|
||||
|
@ -145,7 +145,7 @@ a rule is already defined.
|
|||
When an [eligible approver](#eligible-approvers) approves a merge request, it will
|
||||
reduce the number of approvals left for all rules that the approver belongs to.
|
||||
|
||||
![Approvals premium merge request widget](img/approvals_premium_mr_widget.png)
|
||||
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v12_7.png)
|
||||
|
||||
## Adding or removing an approval
|
||||
|
||||
|
|
|
@ -10,54 +10,12 @@ module Gitlab
|
|||
class CurrentUserMode
|
||||
NotRequestedError = Class.new(StandardError)
|
||||
|
||||
# RequestStore entries
|
||||
CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze
|
||||
CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY = { res: :current_user_mode, data: :current_admin }.freeze
|
||||
|
||||
# SessionStore entries
|
||||
SESSION_STORE_KEY = :current_user_mode
|
||||
ADMIN_MODE_START_TIME_KEY = :admin_mode
|
||||
ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested
|
||||
ADMIN_MODE_START_TIME_KEY = 'admin_mode'
|
||||
ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested'
|
||||
MAX_ADMIN_MODE_TIME = 6.hours
|
||||
ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes
|
||||
|
||||
class << self
|
||||
# Admin mode activation requires storing a flag in the user session. Using this
|
||||
# method when scheduling jobs in Sidekiq will bypass the session check for a
|
||||
# user that was already in admin mode
|
||||
def bypass_session!(admin_id)
|
||||
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id
|
||||
|
||||
Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")
|
||||
|
||||
yield
|
||||
ensure
|
||||
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
|
||||
end
|
||||
|
||||
def bypass_session_admin_id
|
||||
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY]
|
||||
end
|
||||
|
||||
# Store in the current request the provided user model (only if in admin mode)
|
||||
# and yield
|
||||
def with_current_admin(admin)
|
||||
return yield unless self.new(admin).admin_mode?
|
||||
|
||||
Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin
|
||||
|
||||
Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}")
|
||||
|
||||
yield
|
||||
ensure
|
||||
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY)
|
||||
end
|
||||
|
||||
def current_admin
|
||||
Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
@ -84,7 +42,7 @@ module Gitlab
|
|||
|
||||
raise NotRequestedError unless admin_mode_requested?
|
||||
|
||||
reset_request_store_cache_entries
|
||||
reset_request_store
|
||||
|
||||
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
|
||||
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
|
||||
|
@ -97,7 +55,7 @@ module Gitlab
|
|||
def disable_admin_mode!
|
||||
return unless user&.admin?
|
||||
|
||||
reset_request_store_cache_entries
|
||||
reset_request_store
|
||||
|
||||
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
|
||||
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
|
||||
|
@ -106,7 +64,7 @@ module Gitlab
|
|||
def request_admin_mode!
|
||||
return unless user&.admin?
|
||||
|
||||
reset_request_store_cache_entries
|
||||
reset_request_store
|
||||
|
||||
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
|
||||
end
|
||||
|
@ -115,12 +73,10 @@ module Gitlab
|
|||
|
||||
attr_reader :user
|
||||
|
||||
# RequestStore entry to cache #admin_mode? result
|
||||
def admin_mode_rs_key
|
||||
@admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
|
||||
end
|
||||
|
||||
# RequestStore entry to cache #admin_mode_requested? result
|
||||
def admin_mode_requested_rs_key
|
||||
@admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
|
||||
end
|
||||
|
@ -130,7 +86,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def any_session_with_admin_mode?
|
||||
return true if bypass_session?
|
||||
return true if current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
|
||||
|
||||
all_sessions.any? do |session|
|
||||
|
@ -148,11 +103,7 @@ module Gitlab
|
|||
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
|
||||
end
|
||||
|
||||
def bypass_session?
|
||||
user&.id && user.id == self.class.bypass_session_admin_id
|
||||
end
|
||||
|
||||
def reset_request_store_cache_entries
|
||||
def reset_request_store
|
||||
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
|
||||
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
|
||||
end
|
||||
|
|
|
@ -93,6 +93,8 @@ module Gitlab
|
|||
#
|
||||
redis.expire(key, EXPIRATION)
|
||||
end
|
||||
|
||||
record_metrics(redis.memory("USAGE", key))
|
||||
end
|
||||
|
||||
# Subsequent read_file calls would need the latest cache.
|
||||
|
@ -101,6 +103,10 @@ module Gitlab
|
|||
clear_memoization(:cacheable_files)
|
||||
end
|
||||
|
||||
def record_metrics(memory_usage)
|
||||
self.class.gitlab_redis_diff_caching_memory_usage_bytes.observe({}, memory_usage)
|
||||
end
|
||||
|
||||
def file_paths
|
||||
strong_memoize(:file_paths) do
|
||||
diff_files.collect(&:file_path)
|
||||
|
|
|
@ -17,7 +17,6 @@ module Gitlab
|
|||
chain.add Gitlab::SidekiqMiddleware::BatchLoader
|
||||
chain.add Labkit::Middleware::Sidekiq::Server
|
||||
chain.add Gitlab::SidekiqMiddleware::InstrumentationLogger
|
||||
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
|
||||
chain.add Gitlab::SidekiqStatus::ServerMiddleware
|
||||
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
|
||||
end
|
||||
|
@ -32,7 +31,6 @@ module Gitlab
|
|||
chain.add Gitlab::SidekiqMiddleware::ClientMetrics
|
||||
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
|
||||
chain.add Labkit::Middleware::Sidekiq::Client
|
||||
chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
module AdminMode
|
||||
# Checks if admin mode is enabled for the request creating the sidekiq job
|
||||
# by examining if admin mode has been enabled for the user
|
||||
# If enabled then it injects a job field that persists through the job execution
|
||||
class Client
|
||||
def call(_worker_class, job, _queue, _redis_pool)
|
||||
return yield unless Feature.enabled?(:user_mode_in_session)
|
||||
|
||||
# Admin mode enabled in the original request or in a nested sidekiq job
|
||||
admin_mode_user_id = find_admin_user_id
|
||||
|
||||
if admin_mode_user_id
|
||||
job['admin_mode_user_id'] ||= admin_mode_user_id
|
||||
|
||||
Gitlab::AppLogger.debug("AdminMode::Client injected admin mode for job: #{job.inspect}")
|
||||
end
|
||||
|
||||
yield
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_admin_user_id
|
||||
Gitlab::Auth::CurrentUserMode.current_admin&.id ||
|
||||
Gitlab::Auth::CurrentUserMode.bypass_session_admin_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,24 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
module AdminMode
|
||||
class Server
|
||||
def call(_worker, job, _queue)
|
||||
return yield unless Feature.enabled?(:user_mode_in_session)
|
||||
|
||||
admin_mode_user_id = job['admin_mode_user_id']
|
||||
|
||||
# Do not bypass session if this job was not enabled with admin mode on
|
||||
return yield unless admin_mode_user_id
|
||||
|
||||
Gitlab::Auth::CurrentUserMode.bypass_session!(admin_mode_user_id) do
|
||||
Gitlab::AppLogger.debug("AdminMode::Server bypasses session for admin mode in job: #{job.inspect}")
|
||||
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7137,6 +7137,21 @@ msgstr ""
|
|||
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "EnableReviewApp|%{stepStart}Step 2%{stepEnd}. Copy the following snippet:"
|
||||
msgstr ""
|
||||
|
||||
msgid "EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file."
|
||||
msgstr ""
|
||||
|
||||
msgid "EnableReviewApp|Close"
|
||||
msgstr ""
|
||||
|
||||
msgid "EnableReviewApp|Copy snippet text"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7311,6 +7326,9 @@ msgstr ""
|
|||
msgid "Environments|Deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Enable review app"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Environment"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11529,6 +11547,9 @@ msgstr ""
|
|||
msgid "Manage two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage your license"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manifest"
|
||||
msgstr ""
|
||||
|
||||
|
@ -16189,6 +16210,9 @@ msgstr ""
|
|||
msgid "Review time is defined as the time it takes from first comment until merged."
|
||||
msgstr ""
|
||||
|
||||
msgid "ReviewApp|Enable Review App"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reviewing"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19205,6 +19229,9 @@ msgstr ""
|
|||
msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
|
||||
msgstr ""
|
||||
|
||||
msgid "This GitLab instance is licensed at the %{insufficient_license} tier. Geo is only available for users who have at least a Premium license."
|
||||
msgstr ""
|
||||
|
||||
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21868,9 +21895,6 @@ msgstr ""
|
|||
msgid "You need a different license to enable FileLocks feature"
|
||||
msgstr ""
|
||||
|
||||
msgid "You need a different license to use Geo replication."
|
||||
msgstr ""
|
||||
|
||||
msgid "You need git-lfs version %{min_git_lfs_version} (or greater) to continue. Please visit https://git-lfs.github.com"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -30,9 +30,9 @@ module QA::Page
|
|||
element :pipeline_badges
|
||||
end
|
||||
|
||||
def running?
|
||||
def running?(wait: 0)
|
||||
within('.ci-header-container') do
|
||||
page.has_content?('running')
|
||||
page.has_content?('running', wait: wait)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,78 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Verify', :orchestrated, :docker do
|
||||
context 'Verify', :docker do
|
||||
describe 'Pipeline creation and processing' do
|
||||
let(:executor) { "qa-runner-#{Time.now.to_i}" }
|
||||
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'project-with-pipeline'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Resource::Runner.fabricate! do |runner|
|
||||
runner.project = project
|
||||
runner.name = executor
|
||||
runner.tags = [executor]
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Service::DockerRun::GitlabRunner.new(executor).remove!
|
||||
end
|
||||
|
||||
it 'users creates a pipeline which gets processed' do
|
||||
it 'users creates a pipeline which gets processed', :smoke do
|
||||
Flow::Login.sign_in
|
||||
|
||||
project = Resource::Project.fabricate! do |project|
|
||||
project.name = 'project-with-pipelines'
|
||||
project.description = 'Project with CI/CD Pipelines.'
|
||||
end
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~YAML
|
||||
test-success:
|
||||
tags:
|
||||
- #{executor}
|
||||
script: echo 'OK'
|
||||
|
||||
Resource::Runner.fabricate! do |runner|
|
||||
runner.project = project
|
||||
runner.name = executor
|
||||
runner.tags = %w[qa test]
|
||||
end
|
||||
test-failure:
|
||||
tags:
|
||||
- #{executor}
|
||||
script:
|
||||
- echo 'FAILURE'
|
||||
- exit 1
|
||||
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.file_name = '.gitlab-ci.yml'
|
||||
push.commit_message = 'Add .gitlab-ci.yml'
|
||||
push.file_content = <<~EOF
|
||||
test-success:
|
||||
tags:
|
||||
- qa
|
||||
- test
|
||||
script: echo 'OK'
|
||||
test-tags:
|
||||
tags:
|
||||
- invalid
|
||||
script: echo 'NOOP'
|
||||
|
||||
test-failure:
|
||||
tags:
|
||||
- qa
|
||||
- test
|
||||
script:
|
||||
- echo 'FAILURE'
|
||||
- exit 1
|
||||
|
||||
test-tags:
|
||||
tags:
|
||||
- qa
|
||||
- docker
|
||||
script: echo 'NOOP'
|
||||
|
||||
test-artifacts:
|
||||
tags:
|
||||
- qa
|
||||
- test
|
||||
script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt
|
||||
artifacts:
|
||||
paths:
|
||||
- my-artifacts/
|
||||
EOF
|
||||
test-artifacts:
|
||||
tags:
|
||||
- #{executor}
|
||||
script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt
|
||||
artifacts:
|
||||
paths:
|
||||
- my-artifacts/
|
||||
YAML
|
||||
}
|
||||
]
|
||||
)
|
||||
end.project.visit!
|
||||
|
||||
expect(page).to have_content('Add .gitlab-ci.yml')
|
||||
|
||||
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
|
||||
|
||||
expect(page).to have_content('All 1')
|
||||
expect(page).to have_content('Add .gitlab-ci.yml')
|
||||
|
||||
puts 'Waiting for the runner to process the pipeline'
|
||||
sleep 15 # Runner should process all jobs within 15 seconds.
|
||||
|
||||
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
expect(pipeline).to be_running
|
||||
expect(pipeline).to be_running(wait: 30)
|
||||
expect(pipeline).to have_build('test-success', status: :success)
|
||||
expect(pipeline).to have_build('test-failure', status: :failed)
|
||||
expect(pipeline).to have_build('test-tags', status: :pending)
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
# Test an operation that triggers background jobs requiring administrative rights
|
||||
describe 'Admin mode for workers', :do_not_mock_admin_mode, :request_store, :clean_gitlab_redis_shared_state do
|
||||
let(:user) { create(:user) }
|
||||
let(:user_to_delete) { create(:user) }
|
||||
|
||||
before do
|
||||
add_sidekiq_middleware
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'as a regular user' do
|
||||
it 'cannot delete user' do
|
||||
visit admin_user_path(user_to_delete)
|
||||
|
||||
expect(page).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an admin user' do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it 'cannot delete user', :js do
|
||||
visit admin_user_path(user_to_delete)
|
||||
|
||||
expect(page).to have_content('Re-authentication required')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode enabled', :delete do
|
||||
before do
|
||||
gitlab_enable_admin_mode_sign_in(user)
|
||||
end
|
||||
|
||||
it 'can delete user', :js do
|
||||
visit admin_user_path(user_to_delete)
|
||||
click_button 'Delete user'
|
||||
|
||||
page.within '.modal-dialog' do
|
||||
find("input[name='username']").send_keys(user_to_delete.name)
|
||||
click_button 'Delete user'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
expect(page).to have_content('The user is being deleted.')
|
||||
|
||||
# Perform jobs while logged out so that admin mode is only enabled in job metadata
|
||||
execute_jobs_signed_out(user)
|
||||
|
||||
visit admin_user_path(user_to_delete)
|
||||
|
||||
expect(page).to have_title('Not Found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_sidekiq_middleware
|
||||
Sidekiq::Testing.server_middleware do |chain|
|
||||
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
|
||||
end
|
||||
end
|
||||
|
||||
def execute_jobs_signed_out(user)
|
||||
gitlab_sign_out
|
||||
|
||||
Sidekiq::Worker.drain_all
|
||||
|
||||
sign_in(user)
|
||||
gitlab_enable_admin_mode_sign_in(user)
|
||||
end
|
||||
end
|
|
@ -2,64 +2,46 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Admin uses repository checks', :request_store, :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
|
||||
describe 'Admin uses repository checks' do
|
||||
include StubENV
|
||||
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
|
||||
sign_in(admin)
|
||||
sign_in(create(:admin))
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it 'admin project page requires admin mode' do
|
||||
project = create(:project)
|
||||
visit_admin_project_page(project)
|
||||
it 'to trigger a single check' do
|
||||
project = create(:project)
|
||||
visit_admin_project_page(project)
|
||||
|
||||
expect(page).not_to have_css('.repository-check')
|
||||
expect(page).to have_content('Enter Admin Mode')
|
||||
page.within('.repository-check') do
|
||||
click_button 'Trigger repository check'
|
||||
end
|
||||
|
||||
expect(page).to have_content('Repository check was triggered')
|
||||
end
|
||||
|
||||
it 'to see a single failed repository check', :js do
|
||||
project = create(:project)
|
||||
project.update_columns(
|
||||
last_repository_check_failed: true,
|
||||
last_repository_check_at: Time.now
|
||||
)
|
||||
visit_admin_project_page(project)
|
||||
|
||||
page.within('.alert') do
|
||||
expect(page.text).to match(/Last repository check \(just now\) failed/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode is enabled' do
|
||||
before do
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
end
|
||||
it 'to clear all repository checks', :js do
|
||||
visit repository_admin_application_settings_path
|
||||
|
||||
it 'to trigger a single check', :js do
|
||||
project = create(:project)
|
||||
visit_admin_project_page(project)
|
||||
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
|
||||
|
||||
page.within('.repository-check') do
|
||||
click_button 'Trigger repository check'
|
||||
end
|
||||
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
|
||||
|
||||
expect(page).to have_content('Repository check was triggered')
|
||||
end
|
||||
|
||||
it 'to see a single failed repository check', :js do
|
||||
project = create(:project)
|
||||
project.update_columns(
|
||||
last_repository_check_failed: true,
|
||||
last_repository_check_at: Time.now
|
||||
)
|
||||
visit_admin_project_page(project)
|
||||
|
||||
page.within('.alert') do
|
||||
expect(page.text).to match(/Last repository check \(just now\) failed/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'to clear all repository checks', :js do
|
||||
visit repository_admin_application_settings_path
|
||||
|
||||
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
|
||||
|
||||
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
|
||||
|
||||
expect(page).to have_content('Started asynchronous removal of all repository check states.')
|
||||
end
|
||||
expect(page).to have_content('Started asynchronous removal of all repository check states.')
|
||||
end
|
||||
|
||||
def visit_admin_project_page(project)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
import EnableReviewAppButton from '~/environments/components/enable_review_app_button.vue';
|
||||
|
||||
describe('Enable Review App Button', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('renders button with text', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(EnableReviewAppButton);
|
||||
});
|
||||
|
||||
it('renders Enable Review text', () => {
|
||||
expect(wrapper.text()).toBe('Enable review app');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renders the modal', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(EnableReviewAppButton);
|
||||
});
|
||||
|
||||
it('renders the copyToClipboard button', () => {
|
||||
expect(wrapper.find(ModalCopyButton).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -55,6 +55,26 @@ describe('Environment', () => {
|
|||
"You don't have any environments right now",
|
||||
);
|
||||
});
|
||||
|
||||
describe('when it is possible to enable a review app', () => {
|
||||
beforeEach(done => {
|
||||
mock
|
||||
.onGet(mockData.endpoint)
|
||||
.reply(200, { environments: [], review_app: { can_setup_review_app: true } });
|
||||
|
||||
component = mountComponent(EnvironmentsComponent, mockData);
|
||||
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('should render the enable review app button', () => {
|
||||
expect(component.$el.querySelector('.js-enable-review-app-button').textContent).toContain(
|
||||
'Enable review app',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with paginated environments', () => {
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store do
|
||||
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
|
||||
include_context 'custom session'
|
||||
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let(:user) { build(:user) }
|
||||
|
||||
subject { described_class.new(user) }
|
||||
|
||||
|
@ -13,66 +13,54 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
|
|||
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
|
||||
end
|
||||
|
||||
shared_examples 'admin mode cannot be enabled' do
|
||||
it 'is false by default' do
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
describe '#admin_mode?', :request_store do
|
||||
context 'when the user is a regular user' do
|
||||
it 'is false by default' do
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'cannot be enabled with a valid password' do
|
||||
subject.enable_admin_mode!(password: user.password)
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'cannot be enabled with an invalid password' do
|
||||
subject.enable_admin_mode!(password: nil)
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'cannot be enabled with empty params' do
|
||||
subject.enable_admin_mode!
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'disable has no effect' do
|
||||
subject.enable_admin_mode!
|
||||
subject.disable_admin_mode!
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
context 'skipping password validation' do
|
||||
it 'cannot be enabled with a valid password' do
|
||||
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
|
||||
subject.enable_admin_mode!(password: user.password)
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'cannot be enabled with an invalid password' do
|
||||
subject.enable_admin_mode!(skip_password_validation: true)
|
||||
subject.enable_admin_mode!(password: nil)
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#admin_mode?' do
|
||||
context 'when the user is a regular user' do
|
||||
it_behaves_like 'admin mode cannot be enabled'
|
||||
it 'cannot be enabled with empty params' do
|
||||
subject.enable_admin_mode!
|
||||
|
||||
context 'bypassing session' do
|
||||
it_behaves_like 'admin mode cannot be enabled' do
|
||||
around do |example|
|
||||
described_class.bypass_session!(user.id) { example.run }
|
||||
end
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'disable has no effect' do
|
||||
subject.enable_admin_mode!
|
||||
subject.disable_admin_mode!
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
context 'skipping password validation' do
|
||||
it 'cannot be enabled with a valid password' do
|
||||
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
it 'cannot be enabled with an invalid password' do
|
||||
subject.enable_admin_mode!(skip_password_validation: true)
|
||||
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is an admin' do
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
let(:user) { build(:user, :admin) }
|
||||
|
||||
context 'when admin mode not requested' do
|
||||
it 'is false by default' do
|
||||
|
@ -160,36 +148,11 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'bypassing session' do
|
||||
it 'is active by default' do
|
||||
described_class.bypass_session!(user.id) do
|
||||
expect(subject.admin_mode?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'enable has no effect' do
|
||||
described_class.bypass_session!(user.id) do
|
||||
subject.request_admin_mode!
|
||||
subject.enable_admin_mode!(password: user.password)
|
||||
|
||||
expect(subject.admin_mode?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'disable has no effect' do
|
||||
described_class.bypass_session!(user.id) do
|
||||
subject.disable_admin_mode!
|
||||
|
||||
expect(subject.admin_mode?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#enable_admin_mode!' do
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
let(:user) { build(:user, :admin) }
|
||||
|
||||
it 'creates a timestamp in the session' do
|
||||
subject.request_admin_mode!
|
||||
|
@ -200,7 +163,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
|
|||
end
|
||||
|
||||
describe '#enable_sessionless_admin_mode!' do
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
let(:user) { build(:user, :admin) }
|
||||
|
||||
it 'enabled admin mode without password' do
|
||||
subject.enable_sessionless_admin_mode!
|
||||
|
@ -210,7 +173,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
|
|||
end
|
||||
|
||||
describe '#disable_admin_mode!' do
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
let(:user) { build(:user, :admin) }
|
||||
|
||||
it 'sets the session timestamp to nil' do
|
||||
subject.request_admin_mode!
|
||||
|
@ -220,73 +183,6 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
|
|||
end
|
||||
end
|
||||
|
||||
describe '.bypass_session!' do
|
||||
context 'with a regular user' do
|
||||
it 'admin mode is false' do
|
||||
described_class.bypass_session!(user.id) do
|
||||
expect(subject.admin_mode?).to be(false)
|
||||
expect(described_class.bypass_session_admin_id).to be(user.id)
|
||||
end
|
||||
|
||||
expect(described_class.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an admin user' do
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
|
||||
it 'admin mode is true' do
|
||||
described_class.bypass_session!(user.id) do
|
||||
expect(subject.admin_mode?).to be(true)
|
||||
expect(described_class.bypass_session_admin_id).to be(user.id)
|
||||
end
|
||||
|
||||
expect(described_class.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_current_request_admin_mode' do
|
||||
context 'with a regular user' do
|
||||
it 'user is not available inside nor outside the yielded block' do
|
||||
described_class.with_current_admin(user) do
|
||||
expect(described_class.current_admin).to be_nil
|
||||
end
|
||||
|
||||
expect(described_class.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an admin user' do
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
|
||||
context 'admin mode is disabled' do
|
||||
it 'user is not available inside nor outside the yielded block' do
|
||||
described_class.with_current_admin(user) do
|
||||
expect(described_class.current_admin).to be_nil
|
||||
end
|
||||
|
||||
expect(described_class.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'admin mode is enabled' do
|
||||
before do
|
||||
subject.request_admin_mode!
|
||||
subject.enable_admin_mode!(password: user.password)
|
||||
end
|
||||
|
||||
it 'user is available only inside the yielded block' do
|
||||
described_class.with_current_admin(user) do
|
||||
expect(described_class.current_admin).to be(user)
|
||||
end
|
||||
|
||||
expect(described_class.current_admin).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expected_session_entry(value_matcher)
|
||||
{
|
||||
Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
|
||||
|
|
|
@ -101,6 +101,13 @@ describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
|
|||
let(:paths) { merge_request.diffs.raw_diff_files.select(&:text?).map(&:file_path) }
|
||||
end
|
||||
|
||||
it 'updates memory usage metrics' do
|
||||
expect(described_class.gitlab_redis_diff_caching_memory_usage_bytes)
|
||||
.to receive(:observe).and_call_original
|
||||
|
||||
cache.send(:write_to_redis_hash, diff_hash)
|
||||
end
|
||||
|
||||
context 'different diff_collections for the same diffable' do
|
||||
before do
|
||||
cache.write_if_empty
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::SidekiqMiddleware::AdminMode::Client, :do_not_mock_admin_mode, :request_store do
|
||||
include AdminModeHelper
|
||||
|
||||
let(:worker) do
|
||||
Class.new do
|
||||
def perform; end
|
||||
end
|
||||
end
|
||||
|
||||
let(:job) { {} }
|
||||
let(:queue) { :test }
|
||||
|
||||
it 'yields block' do
|
||||
expect do |b|
|
||||
subject.call(worker, job, queue, nil, &b)
|
||||
end.to yield_control.once
|
||||
end
|
||||
|
||||
context 'user is a regular user' do
|
||||
it 'no admin mode field in payload' do
|
||||
subject.call(worker, job, queue, nil) { nil }
|
||||
|
||||
expect(job).not_to include('admin_mode_user_id')
|
||||
end
|
||||
end
|
||||
|
||||
context 'user is an administrator' do
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
context 'admin mode disabled' do
|
||||
it 'no admin mode field in payload' do
|
||||
subject.call(worker, job, queue, nil) { nil }
|
||||
|
||||
expect(job).not_to include('admin_mode_user_id')
|
||||
end
|
||||
end
|
||||
|
||||
context 'admin mode enabled' do
|
||||
before do
|
||||
enable_admin_mode!(admin)
|
||||
end
|
||||
|
||||
context 'when sidekiq required context not set' do
|
||||
it 'no admin mode field in payload' do
|
||||
subject.call(worker, job, queue, nil) { nil }
|
||||
|
||||
expect(job).not_to include('admin_mode_user_id')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user stored in current request' do
|
||||
it 'has admin mode field in payload' do
|
||||
Gitlab::Auth::CurrentUserMode.with_current_admin(admin) do
|
||||
subject.call(worker, job, queue, nil) { nil }
|
||||
|
||||
expect(job).to include('admin_mode_user_id' => admin.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when bypassing session' do
|
||||
it 'has admin mode field in payload' do
|
||||
Gitlab::Auth::CurrentUserMode.bypass_session!(admin.id) do
|
||||
subject.call(worker, job, queue, nil) { nil }
|
||||
|
||||
expect(job).to include('admin_mode_user_id' => admin.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'admin mode feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(user_mode_in_session: false)
|
||||
end
|
||||
|
||||
it 'yields block' do
|
||||
expect do |b|
|
||||
subject.call(worker, job, queue, nil, &b)
|
||||
end.to yield_control.once
|
||||
end
|
||||
|
||||
it 'no admin mode field in payload' do
|
||||
subject.call(worker, job, queue, nil) { nil }
|
||||
|
||||
expect(job).not_to include('admin_mode_user_id')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,72 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::SidekiqMiddleware::AdminMode::Server, :do_not_mock_admin_mode, :request_store do
|
||||
include AdminModeHelper
|
||||
|
||||
let(:worker) do
|
||||
Class.new do
|
||||
def perform; end
|
||||
end
|
||||
end
|
||||
|
||||
let(:job) { {} }
|
||||
let(:queue) { :test }
|
||||
|
||||
it 'yields block' do
|
||||
expect do |b|
|
||||
subject.call(worker, job, queue, &b)
|
||||
end.to yield_control.once
|
||||
end
|
||||
|
||||
context 'job has no admin mode field' do
|
||||
it 'session is not bypassed' do
|
||||
subject.call(worker, job, queue) do
|
||||
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'job has admin mode field' do
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
context 'nil admin mode id' do
|
||||
let(:job) { { 'admin_mode_user_id' => nil } }
|
||||
|
||||
it 'session is not bypassed' do
|
||||
subject.call(worker, job, queue) do
|
||||
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'valid admin mode id' do
|
||||
let(:job) { { 'admin_mode_user_id' => admin.id } }
|
||||
|
||||
it 'session is bypassed' do
|
||||
subject.call(worker, job, queue) do
|
||||
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be(admin.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'admin mode feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(user_mode_in_session: false)
|
||||
end
|
||||
|
||||
it 'yields block' do
|
||||
expect do |b|
|
||||
subject.call(worker, job, queue, &b)
|
||||
end.to yield_control.once
|
||||
end
|
||||
|
||||
it 'session is not bypassed' do
|
||||
subject.call(worker, job, queue) do
|
||||
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -45,8 +45,7 @@ describe Gitlab::SidekiqMiddleware do
|
|||
Gitlab::SidekiqMiddleware::ArgumentsLogger,
|
||||
Gitlab::SidekiqMiddleware::MemoryKiller,
|
||||
Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
|
||||
Gitlab::SidekiqMiddleware::WorkerContext::Server,
|
||||
Gitlab::SidekiqMiddleware::AdminMode::Server
|
||||
Gitlab::SidekiqMiddleware::WorkerContext::Server
|
||||
]
|
||||
end
|
||||
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
|
||||
|
@ -116,8 +115,7 @@ describe Gitlab::SidekiqMiddleware do
|
|||
Gitlab::SidekiqStatus::ClientMiddleware,
|
||||
Gitlab::SidekiqMiddleware::ClientMetrics,
|
||||
Gitlab::SidekiqMiddleware::WorkerContext::Client,
|
||||
Labkit::Middleware::Sidekiq::Client,
|
||||
Gitlab::SidekiqMiddleware::AdminMode::Client
|
||||
Labkit::Middleware::Sidekiq::Client
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -2985,9 +2985,9 @@ describe User, :do_not_mock_admin_mode do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#can_read_all_resources?', :request_store do
|
||||
describe '#can_read_all_resources?' do
|
||||
it 'returns false for regular user' do
|
||||
user = build_stubbed(:user)
|
||||
user = build(:user)
|
||||
|
||||
expect(user.can_read_all_resources?).to be_falsy
|
||||
end
|
||||
|
@ -2995,7 +2995,7 @@ describe User, :do_not_mock_admin_mode do
|
|||
context 'for admin user' do
|
||||
include_context 'custom session'
|
||||
|
||||
let(:user) { build_stubbed(:user, :admin) }
|
||||
let(:user) { build(:user, :admin) }
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it 'returns false' do
|
||||
|
|
|
@ -23,8 +23,8 @@ describe BasePolicy, :do_not_mock_admin_mode do
|
|||
end
|
||||
|
||||
describe 'read cross project' do
|
||||
let(:current_user) { build_stubbed(:user) }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let(:current_user) { create(:user) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new(current_user, [user]) }
|
||||
|
||||
|
@ -38,7 +38,7 @@ describe BasePolicy, :do_not_mock_admin_mode do
|
|||
it { is_expected.not_to be_allowed(:read_cross_project) }
|
||||
|
||||
context 'for admins' do
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
let(:current_user) { build(:admin) }
|
||||
|
||||
subject { described_class.new(current_user, nil) }
|
||||
|
||||
|
@ -56,14 +56,14 @@ describe BasePolicy, :do_not_mock_admin_mode do
|
|||
end
|
||||
|
||||
describe 'full private access' do
|
||||
let(:current_user) { build_stubbed(:user) }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
subject { described_class.new(current_user, nil) }
|
||||
|
||||
it { is_expected.not_to be_allowed(:read_all_resources) }
|
||||
|
||||
context 'for admins' do
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
let(:current_user) { build(:admin) }
|
||||
|
||||
it 'allowed when in admin mode' do
|
||||
enable_admin_mode!(current_user)
|
||||
|
|
|
@ -8,44 +8,40 @@ RSpec.shared_examples 'thread comments' do |resource_name|
|
|||
let(:submit_selector) { "#{form_selector} .js-comment-submit-button" }
|
||||
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
|
||||
let(:comments_selector) { '.timeline > .note.timeline-entry' }
|
||||
let(:comment) { 'My comment' }
|
||||
|
||||
it 'clicking "Comment" will post a comment', :quarantine do
|
||||
it 'clicking "Comment" will post a comment' do
|
||||
expect(page).to have_selector toggle_selector
|
||||
|
||||
find("#{form_selector} .note-textarea").send_keys('a')
|
||||
find("#{form_selector} .note-textarea").send_keys(comment)
|
||||
|
||||
find(submit_selector).click
|
||||
click_button 'Comment'
|
||||
|
||||
wait_for_requests
|
||||
expect(page).to have_content(comment)
|
||||
|
||||
find(comments_selector, match: :first)
|
||||
new_comment = all(comments_selector).last
|
||||
|
||||
expect(new_comment).to have_content 'a'
|
||||
expect(new_comment).not_to have_selector '.discussion'
|
||||
end
|
||||
|
||||
if resource_name == 'issue'
|
||||
it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do
|
||||
find("#{form_selector} .note-textarea").send_keys('a')
|
||||
find("#{form_selector} .note-textarea").send_keys(comment)
|
||||
|
||||
find(close_selector).click
|
||||
wait_for_requests
|
||||
click_button 'Comment & close issue'
|
||||
|
||||
find(comments_selector, match: :first)
|
||||
find("#{comments_selector}.system-note")
|
||||
entries = all(comments_selector)
|
||||
close_note = entries.last
|
||||
new_comment = entries[-2]
|
||||
expect(page).to have_content(comment)
|
||||
expect(page).to have_content "@#{user.username} closed"
|
||||
|
||||
new_comment = all(comments_selector).last
|
||||
|
||||
expect(close_note).to have_content 'closed'
|
||||
expect(new_comment).not_to have_selector '.discussion'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the toggle is clicked' do
|
||||
before do
|
||||
find("#{form_selector} .note-textarea").send_keys('a')
|
||||
find("#{form_selector} .note-textarea").send_keys(comment)
|
||||
|
||||
find(toggle_selector).click
|
||||
end
|
||||
|
@ -153,10 +149,11 @@ RSpec.shared_examples 'thread comments' do |resource_name|
|
|||
end
|
||||
|
||||
it 'clicking "Start thread" will post a thread' do
|
||||
expect(page).to have_content(comment)
|
||||
|
||||
new_comment = all(comments_selector).last
|
||||
|
||||
expect(new_comment).to have_content 'a'
|
||||
expect(new_comment).to have_selector '.discussion'
|
||||
expect(new_comment).to have_selector('.discussion')
|
||||
end
|
||||
|
||||
if resource_name =~ /(issue|merge request)/
|
||||
|
@ -208,15 +205,13 @@ RSpec.shared_examples 'thread comments' do |resource_name|
|
|||
|
||||
if resource_name == 'issue'
|
||||
it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}" do
|
||||
find(close_selector).click
|
||||
click_button 'Start thread & close issue'
|
||||
|
||||
find(comments_selector, match: :first)
|
||||
find("#{comments_selector}.system-note")
|
||||
entries = all(comments_selector)
|
||||
close_note = entries.last
|
||||
new_discussion = entries[-2]
|
||||
expect(page).to have_content(comment)
|
||||
expect(page).to have_content "@#{user.username} closed"
|
||||
|
||||
new_discussion = all(comments_selector)[-2]
|
||||
|
||||
expect(close_note).to have_content 'closed'
|
||||
expect(new_discussion).to have_selector '.discussion'
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue