diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index ce1fa328b69..676f402f7cf 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -180,6 +180,12 @@ the steps bellow.
Feature.enable(:repository_push_audit_event)
```
+## Retention policy
+
+On GitLab.com, Audit Event records become subject to deletion after 400 days, or when your license is downgraded to a tier that does not include access to Audit Events. Data that is subject to deletion will be deleted at GitLab's discretion, possibly without additional notice.
+
+If you require a longer retention period, you should independently archive your Audit Event data, which you can retrieve through the [Audit Events API](../api/audit_events.md).
+
## Export to CSV **(PREMIUM ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1449) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 3b0beafa35a..0aa94b86371 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -143,7 +143,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Container Registry](packages/container_registry.md): Configure Container Registry with GitLab.
- [Package Registry](packages/index.md): Enable GitLab to act as an NPM Registry and a Maven Repository.
-- [Dependency Proxy](packages/dependency_proxy.md): Configure the Dependency Proxy, a local proxy for frequently used upstream images/packages. **(PREMIUM ONLY)**
+- [Dependency Proxy](packages/dependency_proxy.md): Configure the Dependency Proxy, a local proxy for frequently used upstream images/packages.
### Repository settings
diff --git a/doc/administration/packages/dependency_proxy.md b/doc/administration/packages/dependency_proxy.md
index fba3d51f741..7a37bf4dcea 100644
--- a/doc/administration/packages/dependency_proxy.md
+++ b/doc/administration/packages/dependency_proxy.md
@@ -4,9 +4,10 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
-# GitLab Dependency Proxy administration **(PREMIUM ONLY)**
+# GitLab Dependency Proxy administration
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
GitLab can be utilized as a dependency proxy for a variety of common package managers.
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index 26dab5218e2..9f522e0d599 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -2007,7 +2007,7 @@ on what features you intend to use:
| [Merge request diffs](../merge_request_diffs.md#using-object-storage) | Yes |
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
-| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) **(PREMIUM ONLY)** | Yes |
+| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
| [Pseudonymizer](../pseudonymizer.md#configuration) (optional feature) **(ULTIMATE ONLY)** | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 21206907153..b106f7bced1 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -2007,7 +2007,7 @@ on what features you intend to use:
| [Merge request diffs](../merge_request_diffs.md#using-object-storage) | Yes |
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
-| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) **(PREMIUM ONLY)** | Yes |
+| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
| [Pseudonymizer](../pseudonymizer.md#configuration) (optional feature) **(ULTIMATE ONLY)** | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index 9bcc0281cde..f4842a8568b 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -866,7 +866,7 @@ on what features you intend to use:
| [Merge request diffs](../merge_request_diffs.md#using-object-storage) | Yes |
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
-| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) **(PREMIUM ONLY)** | Yes |
+| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
| [Pseudonymizer](../pseudonymizer.md#configuration) (optional feature) **(ULTIMATE ONLY)** | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 9b6bbe19e0e..b5b3e4e0300 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -1742,7 +1742,7 @@ on what features you intend to use:
| [Merge request diffs](../merge_request_diffs.md#using-object-storage) | Yes |
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
-| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) **(PREMIUM ONLY)** | Yes |
+| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
| [Pseudonymizer](../pseudonymizer.md#configuration) (optional feature) **(ULTIMATE ONLY)** | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 5f813c95544..152eb9cb90d 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -2007,7 +2007,7 @@ on what features you intend to use:
| [Merge request diffs](../merge_request_diffs.md#using-object-storage) | Yes |
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
-| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) **(PREMIUM ONLY)** | Yes |
+| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
| [Pseudonymizer](../pseudonymizer.md#configuration) (optional feature) **(ULTIMATE ONLY)** | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index 0e122f564c5..f023971bdc0 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -1741,7 +1741,7 @@ on what features you intend to use:
| [Merge request diffs](../merge_request_diffs.md#using-object-storage) | Yes |
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
-| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) **(PREMIUM ONLY)** | Yes |
+| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
| [Pseudonymizer](../pseudonymizer.md#configuration) (optional feature) **(ULTIMATE ONLY)** | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index de19c025cb4..4108635ba2c 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -1,6 +1,6 @@
---
stage: Plan
-group: Product Planning
+group: Certify
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
diff --git a/doc/api/dependency_proxy.md b/doc/api/dependency_proxy.md
index 7590583bdb6..426d2381858 100644
--- a/doc/api/dependency_proxy.md
+++ b/doc/api/dependency_proxy.md
@@ -4,11 +4,12 @@ group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
-# Dependency Proxy API **(PREMIUM)**
+# Dependency Proxy API
## Purge the dependency proxy for a group
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11631) in GitLab 12.10.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11631) in GitLab 12.10.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
Deletes the cached blobs for a group. This endpoint requires group admin access.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 459cce39ffc..5bb1dc0eff6 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -14967,8 +14967,7 @@ type Project {
): ClusterAgentConnection
"""
- Code coverages summary associated with the project. Available only when
- feature flag `group_coverage_data_report` is enabled
+ Code coverage summary associated with the project
"""
codeCoverageSummary: CodeCoverageSummary
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 3965100cea8..6614396da72 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -44149,7 +44149,7 @@
},
{
"name": "codeCoverageSummary",
- "description": "Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled",
+ "description": "Code coverage summary associated with the project",
"args": [
],
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2480deb197b..34d30e8c7f1 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2257,7 +2257,7 @@ Autogenerated return type of PipelineRetry.
| `boards` | BoardConnection | Boards of the project |
| `clusterAgent` | ClusterAgent | Find a single cluster agent by name |
| `clusterAgents` | ClusterAgentConnection | Cluster agents associated with the project |
-| `codeCoverageSummary` | CodeCoverageSummary | Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled |
+| `codeCoverageSummary` | CodeCoverageSummary | Code coverage summary associated with the project |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks associated with the project |
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png
index 3ee5e39afc0..8082d17ae6a 100644
Binary files a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png and b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png differ
diff --git a/doc/ci/introduction/img/gitlab_workflow_example_11_9.png b/doc/ci/introduction/img/gitlab_workflow_example_11_9.png
index f3fb9444b55..1f11db55f81 100644
Binary files a/doc/ci/introduction/img/gitlab_workflow_example_11_9.png and b/doc/ci/introduction/img/gitlab_workflow_example_11_9.png differ
diff --git a/doc/ci/pipelines/img/manual_job_variables.png b/doc/ci/pipelines/img/manual_job_variables.png
index a5ed351fdcd..63801ade21f 100644
Binary files a/doc/ci/pipelines/img/manual_job_variables.png and b/doc/ci/pipelines/img/manual_job_variables.png differ
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index 32db9a261f5..fd3fe239110 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -48,14 +48,16 @@ for changed files. This saves you time as you don't have to wait for the same er
by CI/CD.
Lefthook relies on a pre-push hook to prevent commits that violate its ruleset.
-If you wish to override this behavior, pass the environment variable `LEFTHOOK=0`.
-That is, `LEFTHOOK=0 git push`.
+To override this behavior, pass the environment variable `LEFTHOOK=0`. That is,
+`LEFTHOOK=0 git push`.
You can also:
- Define [local configuration](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#local-config).
-- Skip [checks per tag on the fly](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#skip-some-tags-on-the-fly), e.g. `LEFTHOOK_EXCLUDE=frontend git push origin`.
-- Run [hooks manually](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#run-githook-group-directly), e.g. `lefthook run pre-push`.
+- Skip [checks per tag on the fly](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#skip-some-tags-on-the-fly).
+ For example, `LEFTHOOK_EXCLUDE=frontend git push origin`.
+- Run [hooks manually](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#run-githook-group-directly).
+ For example, `lefthook run pre-push`.
## Ruby, Rails, RSpec
diff --git a/doc/development/img/memory_ruby_heap_fragmentation.png b/doc/development/img/memory_ruby_heap_fragmentation.png
index 4703da7491d..204130f8d87 100644
Binary files a/doc/development/img/memory_ruby_heap_fragmentation.png and b/doc/development/img/memory_ruby_heap_fragmentation.png differ
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index b05c0e351a9..44c69acbd87 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -181,7 +181,9 @@ SAST and Dependency Scanning scanners must scan the files in the project directo
In order to be consistent with the official Container Scanning for GitLab,
scanners must scan the Docker image whose name and tag are given by
-`CI_APPLICATION_REPOSITORY` and `CI_APPLICATION_TAG`, respectively.
+`CI_APPLICATION_REPOSITORY` and `CI_APPLICATION_TAG`, respectively. If the `DOCKER_IMAGE`
+variable is provided, then the `CI_APPLICATION_REPOSITORY` and `CI_APPLICATION_TAG` variables
+are ignored, and the image specified in the `DOCKER_IMAGE` variable is scanned instead.
If not provided, `CI_APPLICATION_REPOSITORY` should default to
`$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG`, which is a combination of predefined CI variables.
diff --git a/doc/development/testing_guide/end_to_end/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md
index 792e5ef1d6f..3ee4a3d1b87 100644
--- a/doc/development/testing_guide/end_to_end/best_practices.md
+++ b/doc/development/testing_guide/end_to_end/best_practices.md
@@ -325,39 +325,69 @@ In general, we use an `expect` statement to check that something _is_ as we expe
```ruby
Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).to have_job("a_job")
+ expect(pipeline).to have_job('a_job')
end
```
-### Ensure `expect` checks for negation efficiently
+### Create negatable matchers to speed `expect` checks
However, sometimes we want to check that something is _not_ as we _don't_ want it to be. In other
-words, we want to make sure something is absent. In such a case we should use an appropriate
-predicate method that returns quickly, rather than waiting for a state that won't appear.
-
-It's most efficient to use a predicate method that returns immediately when there is no job, or waits
-until it disappears:
+words, we want to make sure something is absent. For unit tests and feature specs,
+we commonly use `not_to`
+because RSpec's built-in matchers are negatable, as are Capybara's, which means the following two statements are
+equivalent.
```ruby
-# Good
-Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).to have_no_job("a_job")
+except(page).not_to have_text('hidden')
+except(page).to have_no_text('hidden')
+```
+
+Unfortunately, that's not automatically the case for the predicate methods that we add to our
+[page objects](page_objects.md). We need to [create our own negatable matchers](https://relishapp.com/rspec/rspec-expectations/v/3-9/docs/custom-matchers/define-a-custom-matcher#matcher-with-separate-logic-for-expect().to-and-expect().not-to).
+
+The initial example uses the `have_job` matcher which is derived from the [`has_job?` predicate
+method of the `Page::Project::Pipeline::Show` page object](https://gitlab.com/gitlab-org/gitlab/-/blob/87864b3047c23b4308f59c27a3757045944af447/qa/qa/page/project/pipeline/show.rb#L53).
+To create a negatable matcher, we use `has_no_job?` for the negative case:
+
+```ruby
+RSpec::Matchers.define :have_job do |job_name|
+ match do |page_object|
+ page_object.has_job?(job_name)
+ end
+
+ match_when_negated do |page_object|
+ page_object.has_no_job?(job_name)
+ end
end
```
-### Problematic alternatives
+And then the two `expect` statements in the following example are equivalent:
-Alternatively, if we want to check that a job doesn't exist it might be tempting to use `not_to`:
+```ruby
+Page::Project::Pipeline::Show.perform do |pipeline|
+ expect(pipeline).not_to have_job('a_job')
+ expect(pipeline).to have_no_job('a_job')
+end
+```
+
+[See this merge request for a real example of adding a custom matcher](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46302).
+
+NOTE: **Note:**
+We need to create custom negatable matchers only for the predicate methods we've added to the test framework, and only if we're using `not_to`. If we use `to have_no_*` a negatable matcher is not necessary.
+
+### Why we need negatable matchers
+
+Consider the following code, but assume that we _don't_ have a custom negatable matcher for `have_job`.
```ruby
# Bad
Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).not_to have_job("a_job")
+ expect(pipeline).not_to have_job('a_job')
end
```
-For this statement to pass, `have_job("a_job")` has to return `false` so that `not_to` can negate it.
-The problem is that `have_job("a_job")` waits up to ten seconds for `"a job"` to appear before
+For this statement to pass, `have_job('a_job')` has to return `false` so that `not_to` can negate it.
+The problem is that `have_job('a_job')` waits up to ten seconds for `'a job'` to appear before
returning `false`. Under the expected condition this test will take ten seconds longer than it needs to.
Instead, we could force no wait:
@@ -365,9 +395,13 @@ Instead, we could force no wait:
```ruby
# Not as bad but potentially flaky
Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).not_to have_job("a_job", wait: 0)
+ expect(pipeline).not_to have_job('a_job', wait: 0)
end
```
-The problem is that if `"a_job"` is present and we're waiting for it to disappear, this statement
-will fail.
+The problem is that if `'a_job'` is present and we're waiting for it to disappear, this statement will fail.
+
+Neither problem is present if we create a custom negatable matcher because the `has_no_job?` predicate method
+would be used, which would wait only as long as necessary for the job to disappear.
+
+Lastly, negatable matchers are preferred over using matchers of the form `have_no_*` because it's a common and familiar practice to negate matchers using `not_to`. If we facilitate that practice by adding negatable matchers, we make it easier for subsequent test authors to write efficient tests.
diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png
index 43e267daff4..693ea2a55cd 100644
Binary files a/doc/integration/img/spam_log.png and b/doc/integration/img/spam_log.png differ
diff --git a/doc/integration/img/submit_issue.png b/doc/integration/img/submit_issue.png
index e794eac189e..c1bb725cc03 100644
Binary files a/doc/integration/img/submit_issue.png and b/doc/integration/img/submit_issue.png differ
diff --git a/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6.png b/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6.png
index dfe5847c9b8..da9e4c64e12 100644
Binary files a/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6.png and b/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6.png differ
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index ea3a666d1b1..eef15a9c424 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -186,6 +186,7 @@ scanning by using the following environment variables:
| `CI_APPLICATION_REPOSITORY` | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | Docker repository URL for the image to be scanned. |
| `CI_APPLICATION_TAG` | `$CI_COMMIT_SHA` | Docker repository tag for the image to be scanned. |
| `CS_MAJOR_VERSION` | `3` | The major version of the Docker image tag. |
+| `DOCKER_IMAGE` | `$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG` | The Docker image to be scanned. If set, this variable overrides the `$CI_APPLICATION_REPOSITORY` and `$CI_APPLICATION_TAG` variables. |
| `DOCKER_INSECURE` | `"false"` | Allow [Klar](https://github.com/optiopay/klar) to access secure Docker registries using HTTPS with bad (or self-signed) SSL certificates. |
| `DOCKER_PASSWORD` | `$CI_REGISTRY_PASSWORD` | Password for accessing a Docker registry requiring authentication. |
| `DOCKER_USER` | `$CI_REGISTRY_USER` | Username for accessing a Docker registry requiring authentication. |
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index 8fa85b91d73..8b1edc609da 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -412,11 +412,56 @@ spec:
## Example projects
+The following example projects can help you get started with the Kubernetes Agent.
+
+### Simple NGINX deployment
+
This basic GitOps example deploys NGINX:
- [Configuration repository](https://gitlab.com/gitlab-org/configure/examples/kubernetes-agent)
- [Manifest repository](https://gitlab.com/gitlab-org/configure/examples/gitops-project)
-- [Install GitLab Runner](https://gitlab.com/gitlab-examples/install-runner-via-k8s-agent)
+
+### Deploying GitLab Runner with the Agent
+
+These instructions assume that the Agent is already set up as described in the
+[Get started with GitOps](#get-started-with-gitops-and-the-gitlab-agent):
+
+1. Check the possible
+ [Runner chart YAML values](https://gitlab.com/gitlab-org/charts/gitlab-runner/blob/master/values.yaml)
+ on the Runner chart documentation, and create a `runner-chart-values.yaml` file
+ with the configuration that fits your needs, such as:
+
+ ```yaml
+ ## The GitLab Server URL (with protocol) that want to register the runner against
+ ## ref: https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-register
+ ##
+ gitlabUrl: https://gitlab.my.domain.com/
+
+ ## The Registration Token for adding new Runners to the GitLab Server. This must
+ ## be retrieved from your GitLab Instance.
+ ## ref: https://docs.gitlab.com/ce/ci/runners/README.html
+ ##
+ runnerRegistrationToken: "XXXXXXYYYYYYZZZZZZ"
+
+ ## For RBAC support:
+ rbac:
+ create: true
+
+ ## Run all containers with the privileged flag enabled
+ ## This will allow the docker:dind image to run if you need to run Docker
+ ## commands. Please read the docs before turning this on:
+ ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-dockerdind
+ runners:
+ privileged: true
+ ```
+
+1. Create a single manifest file to install the Runner chart with your cluster agent:
+
+ ```shell
+ helm template --namespace gitlab gitlab-runner -f runner-chart-values.yaml gitlab/gitlab-runner > manifest.yaml
+ ```
+
+1. Push your `manifest.yaml` to your manifest repository.
## Troubleshooting
@@ -479,7 +524,7 @@ but KAS on the server side is not available via `wss`. To fix it, make sure the
same schemes are configured on both sides.
It's not possible to set the `grpc` scheme due to the issue
-[It is not possible to configure KAS to work with grpc without directly editing GitLab KAS deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/276888). To use `grpc` while the
+[It is not possible to configure KAS to work with `grpc` without directly editing GitLab KAS deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/276888). To use `grpc` while the
issue is in progress, directly edit the deployment with the
`kubectl edit deployment gitlab-kas` command, and change `--listen-websocket=true` to `--listen-websocket=false`. After running that command, you should be able to use
`grpc://gitlab-kas.
:5005`.
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_3_1.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_3_1.png
deleted file mode 100644
index 89f4e917567..00000000000
Binary files a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_3_1.png and /dev/null differ
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png
new file mode 100644
index 00000000000..b2ac4f95e0d
Binary files /dev/null and b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png differ
diff --git a/doc/user/compliance/compliance_dashboard/index.md b/doc/user/compliance/compliance_dashboard/index.md
index d2fb8dd318a..151c61b50d8 100644
--- a/doc/user/compliance/compliance_dashboard/index.md
+++ b/doc/user/compliance/compliance_dashboard/index.md
@@ -17,7 +17,7 @@ for merging into production.
To access the Compliance Dashboard for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu.
-![Compliance Dashboard](img/compliance_dashboard_v13_3_1.png)
+![Compliance Dashboard](img/compliance_dashboard_v13_6.png)
NOTE: **Note:**
The Compliance Dashboard shows only the latest MR on each project.
@@ -63,7 +63,9 @@ This column has four states:
If you do not see the success icon in your Compliance dashboard; please review the above criteria for the Merge Requests
project to make sure it complies with the separation of duties described above.
-## Chain of Custody report
+## Chain of Custody report **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213364) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
The Chain of Custody report allows customers to export a list of merge commits within the group.
The data provides a comprehensive view with respect to merge commits. It includes the merge commit SHA,
@@ -72,6 +74,13 @@ Depending on the merge strategy, the merge commit SHA can either be a merge comm
To download the Chain of Custody report, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu and click **List of all merge commits**
+### Commit-specific Chain of Custody Report **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267629) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
+
+You can generate a commit-specific Chain of Custody report for a given commit SHA. To do so, select
+the dropdown next to the **List of all merge commits** button at the top of the Compliance Dashboard.
+
NOTE: **Note:**
The Chain of Custody report download is a CSV file, with a maximum size of 15 MB.
The remaining records are truncated when this limit is reached.
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 0127e4435f1..e09c685147a 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -798,7 +798,7 @@ With [GitLab Issue Analytics](issues_analytics/index.md), you can see a bar char
With [GitLab Repositories Analytics](repositories_analytics/index.md), you can download a CSV of the latest coverage data for all the projects in your group.
-## Dependency Proxy **(PREMIUM)**
+## Dependency Proxy
Use GitLab as a [dependency proxy](../packages/dependency_proxy/index.md) for upstream Docker images.
diff --git a/doc/user/group/repositories_analytics/index.md b/doc/user/group/repositories_analytics/index.md
index 9c3a4deb2b6..fe5e7979592 100644
--- a/doc/user/group/repositories_analytics/index.md
+++ b/doc/user/group/repositories_analytics/index.md
@@ -15,22 +15,12 @@ This feature might not be available to you. Check the **version history** note a
## Latest project test coverage list
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267624) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
-> - It's [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
-> - It's disabled on GitLab.com
-> - It can be enabled or disabled per-group.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-latest-project-test-coverage).
To see the latest code coverage for each project in your group:
1. Go to **Analytics > Repositories** in the group (not from a project).
1. In the **Latest test coverage results** section, use the **Select projects** dropdown to choose the projects you want to check.
-### Enable or disable latest project test coverage
-
-This feature comes with the `:group_coverage_data_report` feature flag disabled by default. It is disabled on GitLab.com.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can enable it for your instance.
-The group test coverage table can be enabled or disabled per-group.
-
## Download historic test coverage data
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215104) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index f1b49699483..e1fae770a5d 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -4,9 +4,10 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
-# Dependency Proxy **(PREMIUM)**
+# Dependency Proxy
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
upstream images.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 22822268e78..f1365ee1cab 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -256,7 +256,7 @@ group.
| Share (invite) groups with groups | | | | | ✓ |
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
| Create/edit/delete iterations | | | ✓ | ✓ | ✓ |
-| Enable/disable a dependency proxy **(PREMIUM)** | | | ✓ | ✓ | ✓ |
+| Enable/disable a dependency proxy | | | ✓ | ✓ | ✓ |
| Create and edit group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
diff --git a/doc/user/profile/img/busy_status_indicator_v13_6.png b/doc/user/profile/img/busy_status_indicator_v13_6.png
new file mode 100644
index 00000000000..fa945264b8e
Binary files /dev/null and b/doc/user/profile/img/busy_status_indicator_v13_6.png differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index f897cae0b0d..8ae92a42581 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -188,17 +188,22 @@ To set your current status:
1. Set the desired emoji and/or status message.
1. Click **Set status**. Alternatively, you can click **Remove status** to remove your user status entirely.
+![Busy status indicator](img/busy_status_indicator_v13_6.png)
+
or
1. Click your avatar.
1. Select **Profile**.
1. Click **Edit profile** (pencil icon).
1. Enter your status message in the **Your status** text field.
+ 1. Alternatively, select the **Busy** checkbox ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259649) in GitLab 13.6}.
1. Click **Add status emoji** (smiley face), and select the desired emoji.
1. Click **Update profile settings**.
You can also set your current status [using the API](../../api/users.md#user-status).
+If you previously selected the "Busy" checkbox, remember to deselect it when you become available again.
+
## Commit email
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21598) in GitLab 11.4.
diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md
index 09e457fb37f..f533f8807d2 100644
--- a/doc/user/project/requirements/index.md
+++ b/doc/user/project/requirements/index.md
@@ -1,7 +1,7 @@
---
type: reference, howto
stage: Plan
-group: Product Planning
+group: Certify
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index 456466a747d..34a075df990 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -1,6 +1,6 @@
---
stage: Plan
-group: Product Planning
+group: Certify
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 0dafbe492b5..ea149f25584 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -162,6 +162,7 @@ module API
mount ::API::CommitStatuses
mount ::API::ContainerRegistryEvent
mount ::API::ContainerRepositories
+ mount ::API::DependencyProxy
mount ::API::DeployKeys
mount ::API::DeployTokens
mount ::API::Deployments
diff --git a/lib/api/dependency_proxy.rb b/lib/api/dependency_proxy.rb
new file mode 100644
index 00000000000..3379bb2f029
--- /dev/null
+++ b/lib/api/dependency_proxy.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module API
+ class DependencyProxy < ::API::Base
+ helpers ::API::Helpers::PackagesHelpers
+
+ feature_category :dependency_proxy
+
+ helpers do
+ def obtain_new_purge_cache_lease
+ Gitlab::ExclusiveLease
+ .new("dependency_proxy:delete_group_blobs:#{user_group.id}",
+ timeout: 1.hour)
+ .try_obtain
+ end
+ end
+
+ before do
+ authorize! :admin_group, user_group
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Deletes all dependency_proxy_blobs for a group' do
+ detail 'This feature was introduced in GitLab 12.10'
+ end
+ delete ':id/dependency_proxy/cache' do
+ not_found! unless user_group.dependency_proxy_feature_available?
+
+ message = 'This request has already been made. It may take some time to purge the cache. You can run this at most once an hour for a given group'
+ render_api_error!(message, 409) unless obtain_new_purge_cache_lease
+
+ # rubocop:disable CodeReuse/Worker
+ PurgeDependencyProxyCacheWorker.perform_async(current_user.id, user_group.id)
+ # rubocop:enable CodeReuse/Worker
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index 40fcd2f89f2..390da014a5a 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -40,23 +40,35 @@ module Gitlab
private
- def pipeline
- @pipeline ||= @project.ci_pipelines.latest_successful_for_ref(@ref)
+ def successful_pipeline
+ @successful_pipeline ||= @project.ci_pipelines.latest_successful_for_ref(@ref)
+ end
+
+ def failed_pipeline
+ @failed_pipeline ||= @project.ci_pipelines.latest_failed_for_ref(@ref)
+ end
+
+ def running_pipeline
+ @running_pipeline ||= @project.ci_pipelines.latest_running_for_ref(@ref)
end
- # rubocop: disable CodeReuse/ActiveRecord
def raw_coverage
- return unless pipeline
+ latest =
+ if @job.present?
+ builds = ::Ci::Build
+ .in_pipelines([successful_pipeline, running_pipeline, failed_pipeline])
+ .latest
+ .success
+ .for_ref(@ref)
+ .by_name(@job)
- if @job.blank?
- pipeline.coverage
- else
- pipeline.builds
- .find_by(name: @job)
- .try(:coverage)
- end
+ builds.max_by(&:created_at)
+ else
+ successful_pipeline
+ end
+
+ latest&.coverage
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 29a61f7c20a..dfe60fb5a03 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -18,6 +18,8 @@ module Gitlab
attr_reader :octokit
+ SEARCH_MAX_REQUESTS_PER_MINUTE = 30
+
# A single page of data and the corresponding page number.
Page = Struct.new(:objects, :number)
@@ -28,6 +30,7 @@ module Gitlab
# rate limit at once. The threshold is put in place to not hit the limit
# in most cases.
RATE_LIMIT_THRESHOLD = 50
+ SEARCH_RATE_LIMIT_THRESHOLD = 3
# token - The GitHub API token to use.
#
@@ -152,8 +155,26 @@ module Gitlab
end
end
+ def search_repos_by_name(name)
+ each_page(:search_repositories, search_query(str: name, type: :name))
+ end
+
+ def search_query(str:, type:, include_collaborations: true, include_orgs: true)
+ query = "#{str} in:#{type} is:public,private user:#{octokit.user.login}"
+
+ query = [query, collaborations_subquery].join(' ') if include_collaborations
+ query = [query, organizations_subquery].join(' ') if include_orgs
+
+ query
+ end
+
# Returns `true` if we're still allowed to perform API calls.
+ # Search API has rate limit of 30, use lowered threshold when search is used.
def requests_remaining?
+ if requests_limit == SEARCH_MAX_REQUESTS_PER_MINUTE
+ return remaining_requests > SEARCH_RATE_LIMIT_THRESHOLD
+ end
+
remaining_requests > RATE_LIMIT_THRESHOLD
end
@@ -161,6 +182,10 @@ module Gitlab
octokit.rate_limit.remaining
end
+ def requests_limit
+ octokit.rate_limit.limit
+ end
+
def raise_or_wait_for_rate_limit
rate_limit_counter.increment
@@ -221,6 +246,20 @@ module Gitlab
'The number of GitHub API calls performed when importing projects'
)
end
+
+ private
+
+ def collaborations_subquery
+ each_object(:repos, nil, { affiliation: 'collaborator' })
+ .map { |repo| "repo:#{repo.full_name}" }
+ .join(' ')
+ end
+
+ def organizations_subquery
+ each_object(:organizations)
+ .map { |org| "org:#{org.login}" }
+ .join(' ')
+ end
end
end
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 893a3eed689..ad0a5c80604 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -254,6 +254,14 @@ module Gitlab
%r{#{personal_snippet_path_regex}|#{project_snippet_path_regex}}
end
+ def container_image_regex
+ @container_image_regex ||= %r{([\w\.-]+\/){0,1}[\w\.-]+}.freeze
+ end
+
+ def container_image_blob_sha_regex
+ @container_image_blob_sha_regex ||= %r{[\w+.-]+:?\w+}.freeze
+ end
+
private
def personal_snippet_path_regex
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e725c6166c7..4854cccfb3c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14157,6 +14157,9 @@ msgstr ""
msgid "ImportButtons|Connect repositories from"
msgstr ""
+msgid "ImportProjects|%{provider} rate limit exceeded. Try again later"
+msgstr ""
+
msgid "ImportProjects|Blocked import URL: %{message}"
msgstr ""
diff --git a/package.json b/package.json
index c8ade903791..67c9c05e042 100644
--- a/package.json
+++ b/package.json
@@ -162,7 +162,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
- "@gitlab/eslint-plugin": "4.1.0",
+ "@gitlab/eslint-plugin": "5.0.0",
"@testing-library/dom": "^7.16.2",
"@vue/test-utils": "1.0.0-beta.30",
"acorn": "^6.3.0",
diff --git a/spec/controllers/groups/dependency_proxies_controller_spec.rb b/spec/controllers/groups/dependency_proxies_controller_spec.rb
new file mode 100644
index 00000000000..35bd7d47aed
--- /dev/null
+++ b/spec/controllers/groups/dependency_proxies_controller_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::DependencyProxiesController do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ context 'feature enabled' do
+ before do
+ enable_dependency_proxy
+ end
+
+ it 'returns 200 and renders the view' do
+ get :show, params: { group_id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('groups/dependency_proxies/show')
+ end
+ end
+
+ it 'returns 404 when feature is disabled' do
+ disable_dependency_proxy
+
+ get :show, params: { group_id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'PUT #update' do
+ context 'feature enabled' do
+ before do
+ enable_dependency_proxy
+ end
+
+ it 'redirects back to show page' do
+ put :update, params: update_params
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
+ it 'returns 404 when feature is disabled' do
+ put :update, params: update_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ def update_params
+ {
+ group_id: group.to_param,
+ dependency_proxy_group_setting: { enabled: true }
+ }
+ end
+ end
+
+ def enable_dependency_proxy
+ stub_config(dependency_proxy: { enabled: true })
+
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ def disable_dependency_proxy
+ group.create_dependency_proxy_setting!(enabled: false)
+ end
+end
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
new file mode 100644
index 00000000000..615b56ff22f
--- /dev/null
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::DependencyProxyForContainersController do
+ let(:group) { create(:group) }
+ let(:token_response) { { status: :success, token: 'abcd1234' } }
+
+ shared_examples 'not found when disabled' do
+ context 'feature disabled' do
+ before do
+ disable_dependency_proxy
+ end
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ before do
+ allow(Gitlab.config.dependency_proxy)
+ .to receive(:enabled).and_return(true)
+
+ allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance|
+ allow(instance).to receive(:execute).and_return(token_response)
+ end
+ end
+
+ describe 'GET #manifest' do
+ let(:manifest) { { foo: 'bar' }.to_json }
+ let(:pull_response) { { status: :success, manifest: manifest } }
+
+ before do
+ allow_next_instance_of(DependencyProxy::PullManifestService) do |instance|
+ allow(instance).to receive(:execute).and_return(pull_response)
+ end
+ end
+
+ subject { get_manifest }
+
+ context 'feature enabled' do
+ before do
+ enable_dependency_proxy
+ end
+
+ context 'remote token request fails' do
+ let(:token_response) do
+ {
+ status: :error,
+ http_status: 503,
+ message: 'Service Unavailable'
+ }
+ end
+
+ it 'proxies status from the remote token request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ expect(response.body).to eq('Service Unavailable')
+ end
+ end
+
+ context 'remote manifest request fails' do
+ let(:pull_response) do
+ {
+ status: :error,
+ http_status: 400,
+ message: ''
+ }
+ end
+
+ it 'proxies status from the remote manifest request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to be_empty
+ end
+ end
+
+ it 'returns 200 with manifest file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(manifest)
+ end
+ end
+
+ it_behaves_like 'not found when disabled'
+
+ def get_manifest
+ get :manifest, params: { group_id: group.to_param, image: 'alpine', tag: '3.9.2' }
+ end
+ end
+
+ describe 'GET #blob' do
+ let(:blob) { create(:dependency_proxy_blob) }
+ let(:blob_sha) { blob.file_name.sub('.gz', '') }
+ let(:blob_response) { { status: :success, blob: blob } }
+
+ before do
+ allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
+ allow(instance).to receive(:execute).and_return(blob_response)
+ end
+ end
+
+ subject { get_blob }
+
+ context 'feature enabled' do
+ before do
+ enable_dependency_proxy
+ end
+
+ context 'remote blob request fails' do
+ let(:blob_response) do
+ {
+ status: :error,
+ http_status: 400,
+ message: ''
+ }
+ end
+
+ it 'proxies status from the remote blob request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to be_empty
+ end
+ end
+
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(blob.file.path, {})
+
+ subject
+ end
+
+ it 'returns Content-Disposition: attachment' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ end
+ end
+
+ it_behaves_like 'not found when disabled'
+
+ def get_blob
+ get :blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
+ end
+ end
+
+ def enable_dependency_proxy
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ def disable_dependency_proxy
+ group.create_dependency_proxy_setting!(enabled: false)
+ end
+end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index e19b6caca5b..a408d821833 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -144,6 +144,58 @@ RSpec.describe Import::GithubController do
expect(json_response.dig('provider_repos', 0, 'id')).to eq(repo_1.id)
expect(json_response.dig('provider_repos', 1, 'id')).to eq(repo_2.id)
end
+
+ context 'when filtering' do
+ let(:filter) { 'test' }
+ let(:user_login) { 'user' }
+ let(:collaborations_subquery) { 'repo:repo1 repo:repo2' }
+ let(:organizations_subquery) { 'org:org1 org:org2' }
+
+ before do
+ allow_next_instance_of(Octokit::Client) do |client|
+ allow(client).to receive(:user).and_return(double(login: user_login))
+ end
+ end
+
+ it 'makes request to github search api' do
+ expected_query = "test in:name is:public,private user:#{user_login} #{collaborations_subquery} #{organizations_subquery}"
+
+ expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
+ expect(client).to receive(:collaborations_subquery).and_return(collaborations_subquery)
+ expect(client).to receive(:organizations_subquery).and_return(organizations_subquery)
+ expect(client).to receive(:each_page).with(:search_repositories, expected_query).and_return([].to_enum)
+ end
+
+ get :status, params: { filter: filter }, format: :json
+ end
+
+ context 'when user input contains colons and spaces' do
+ before do
+ stub_client(search_repos_by_name: [])
+ end
+
+ it 'sanitizes user input' do
+ filter = ' test1:test2 test3 : test4 '
+ expected_filter = 'test1test2test3test4'
+
+ get :status, params: { filter: filter }, format: :json
+
+ expect(assigns(:filter)).to eq(expected_filter)
+ end
+ end
+
+ context 'when rate limit threshold is exceeded' do
+ before do
+ allow(controller).to receive(:status).and_raise(Gitlab::GithubImport::RateLimitError)
+ end
+
+ it 'returns 429' do
+ get :status, params: { filter: 'test' }, format: :json
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+ end
end
end
diff --git a/spec/factories/dependency_proxy.rb b/spec/factories/dependency_proxy.rb
new file mode 100644
index 00000000000..5d763392a99
--- /dev/null
+++ b/spec/factories/dependency_proxy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :dependency_proxy_blob, class: 'DependencyProxy::Blob' do
+ group
+ file { fixture_file_upload('spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz') }
+ file_name { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz' }
+ end
+end
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
new file mode 100644
index 00000000000..9bbfdc488fb
--- /dev/null
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Group Dependency Proxy' do
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:group) { create(:group) }
+ let(:path) { group_dependency_proxy_path(group) }
+
+ before do
+ group.add_developer(developer)
+ group.add_reporter(reporter)
+
+ enable_feature
+ end
+
+ describe 'feature settings' do
+ context 'when not logged in and feature disabled' do
+ it 'does not show the feature settings' do
+ group.create_dependency_proxy_setting(enabled: false)
+
+ visit path
+
+ expect(page).not_to have_css('.js-dependency-proxy-toggle-area')
+ expect(page).not_to have_css('.js-dependency-proxy-url')
+ end
+ end
+
+ context 'feature is available', :js do
+ context 'when logged in as group developer' do
+ before do
+ sign_in(developer)
+ visit path
+ end
+
+ it 'sidebar menu is open' do
+ sidebar = find('.nav-sidebar')
+ expect(sidebar).to have_link _('Dependency Proxy')
+ end
+
+ it 'toggles defaults to enabled' do
+ page.within('.js-dependency-proxy-toggle-area') do
+ expect(find('.js-project-feature-toggle-input', visible: false).value).to eq('true')
+ end
+ end
+
+ it 'shows the proxy URL' do
+ page.within('.edit_dependency_proxy_group_setting') do
+ expect(find('.js-dependency-proxy-url').value).to have_content('/dependency_proxy/containers')
+ end
+ end
+
+ it 'hides the proxy URL when feature is disabled' do
+ page.within('.edit_dependency_proxy_group_setting') do
+ find('.js-project-feature-toggle').click
+ end
+
+ expect(page).not_to have_css('.js-dependency-proxy-url')
+ expect(find('.js-project-feature-toggle-input', visible: false).value).to eq('false')
+ end
+ end
+
+ context 'when logged in as group reporter' do
+ before do
+ sign_in(reporter)
+ visit path
+ end
+
+ it 'does not show the feature toggle but shows the proxy URL' do
+ expect(page).not_to have_css('.js-dependency-proxy-toggle-area')
+ expect(find('.js-dependency-proxy-url').value).to have_content('/dependency_proxy/containers')
+ end
+ end
+ end
+
+ context 'feature is not avaible' do
+ before do
+ sign_in(developer)
+ end
+
+ context 'group is private' do
+ let(:group) { create(:group, :private) }
+
+ it 'informs user that feature is only available for public groups' do
+ visit path
+
+ expect(page).to have_content('Dependency proxy feature is limited to public groups for now.')
+ end
+ end
+
+ context 'feature is disabled globally' do
+ it 'renders 404 page' do
+ disable_feature
+
+ visit path
+
+ expect(page).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ def enable_feature
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ def disable_feature
+ stub_config(dependency_proxy: { enabled: false })
+ end
+end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index e81f2370d10..dec07eb3783 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -50,6 +50,8 @@ RSpec.describe 'Group navbar' do
insert_package_nav(_('Kubernetes'))
stub_feature_flags(group_iterations: false)
+ stub_config(dependency_proxy: { enabled: false })
+ stub_config(registry: { enabled: false })
stub_group_wikis(false)
group.add_maintainer(user)
sign_in(user)
@@ -73,6 +75,18 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
+ context 'when dependency proxy is available' do
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+
+ insert_dependency_proxy_nav(_('Dependency Proxy'))
+
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
+
context 'when invite team members is not available' do
it 'does not display the js-invite-members-trigger' do
visit group_path(group)
diff --git a/spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz b/spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz
new file mode 100644
index 00000000000..14043720eaf
Binary files /dev/null and b/spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz differ
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 43057353051..067a4ae61a0 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -65,7 +65,7 @@ describe('Blob Header Filepath', () => {
{},
{
scopedSlots: {
- filepathPrepend: `${slotContent}`,
+ 'filepath-prepend': `${slotContent}`,
},
},
);
diff --git a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
index b6e89281fef..744ef318260 100644
--- a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
@@ -73,7 +73,7 @@ exports[`Applications Ingress application shows the correct warning message 1`]
exports[`Applications Knative application shows the correct description 1`] = `
installed via
{
await wrapper.vm.$nextTick();
- expect(findByTestId('installedVia').element).toMatchSnapshot();
+ expect(findByTestId('installed-via').element).toMatchSnapshot();
});
it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index f8af9f40f4c..06afb20c6a2 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -69,6 +69,7 @@ describe('import_projects store actions', () => {
importStatus: STATUSES.NONE,
},
],
+ provider: 'provider',
};
localState.getImportTarget = getImportTarget(localState);
@@ -150,7 +151,28 @@ describe('import_projects store actions', () => {
);
});
- describe('when /home/xanf/projects/gdk/gitlab/spec/frontend/import_projects/store/actions_spec.jsfiltered', () => {
+ describe('when rate limited', () => {
+ it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(429);
+
+ await testAction(
+ fetchRepos,
+ null,
+ { ...localState, filter: 'filter' },
+ [
+ { type: SET_PAGE, payload: 1 },
+ { type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 0 },
+ { type: RECEIVE_REPOS_ERROR },
+ ],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith('Provider rate limit exceeded. Try again later');
+ });
+ });
+
+ describe('when filtered', () => {
it('fetches repos with filter applied', () => {
mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index b758b85beef..dd05f49b458 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -56,7 +56,7 @@ describe('RelatedIssuesBlock', () => {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
- slots: { headerText },
+ slots: { 'header-text': headerText },
});
expect(wrapper.find('.card-title').html()).toContain(headerText);
@@ -72,7 +72,7 @@ describe('RelatedIssuesBlock', () => {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
- slots: { headerActions },
+ slots: { 'header-actions': headerActions },
});
expect(wrapper.find('[data-testid="custom-button"]').html()).toBe(headerActions);
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index ee0e1fd3176..1808faf8f0e 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -106,7 +106,7 @@ describe('Dashboard Panel', () => {
{},
{
slots: {
- topLeft: `OK
`,
+ 'top-left': `OK
`,
},
},
);
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index b7a0ea46b61..27e479ba498 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -508,7 +508,7 @@ describe('Dashboard', () => {
const mockKeyup = key => window.dispatchEvent(new KeyboardEvent('keyup', { key }));
const MockPanel = {
- template: `
`,
+ template: `
`,
};
beforeEach(() => {
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index a620b5d9afc..2d228313a9b 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -244,7 +244,7 @@ describe('Report section', () => {
hasIssues: true,
},
slots: {
- actionButtons: ['Action!'],
+ 'action-buttons': ['Action!'],
},
});
});
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index d943aaf3e5f..0f7c8e97635 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -70,7 +70,7 @@ describe('AlertManagementEmptyState', () => {
...props,
},
slots: {
- 'emtpy-state': EmptyStateSlot,
+ 'empty-state': EmptyStateSlot,
'header-actions': HeaderActionsSlot,
title: TitleSlot,
table: TableSlot,
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index ebe6b8603b1..3b5ea3291e4 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -3,13 +3,15 @@
require 'spec_helper'
RSpec.describe Gitlab::Badge::Coverage::Report do
- let_it_be(:project) { create(:project) }
- let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
+ let_it_be(:running_pipeline) { create(:ci_pipeline, :running, project: project) }
+ let_it_be(:failure_pipeline) { create(:ci_pipeline, :failed, project: project) }
let_it_be(:builds) do
[
- create(:ci_build, :success, pipeline: pipeline, coverage: 40, name: 'first'),
- create(:ci_build, :success, pipeline: pipeline, coverage: 60)
+ create(:ci_build, :success, pipeline: success_pipeline, coverage: 40, created_at: 9.seconds.ago, name: 'coverage'),
+ create(:ci_build, :success, pipeline: success_pipeline, coverage: 60, created_at: 8.seconds.ago)
]
end
@@ -38,28 +40,26 @@ RSpec.describe Gitlab::Badge::Coverage::Report do
end
describe '#status' do
- before do
- allow(badge).to receive(:pipeline).and_return(pipeline)
- end
-
- context 'with no pipeline' do
- let(:pipeline) { nil }
-
- it 'returns nil' do
- expect(badge.status).to be_nil
- end
- end
-
context 'with no job specified' do
- it 'returns the pipeline coverage value' do
+ it 'returns the most recent successful pipeline coverage value' do
expect(badge.status).to eq(50.00)
end
+
+ context 'and no successful pipelines' do
+ before do
+ allow(badge).to receive(:successful_pipeline).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(badge.status).to eq(nil)
+ end
+ end
end
context 'with a blank job name' do
let(:job_name) { ' ' }
- it 'returns the pipeline coverage value' do
+ it 'returns the latest successful pipeline coverage value' do
expect(badge.status).to eq(50.00)
end
end
@@ -73,11 +73,27 @@ RSpec.describe Gitlab::Badge::Coverage::Report do
end
context 'with a matching job name specified' do
- let(:job_name) { 'first' }
+ let(:job_name) { 'coverage' }
it 'returns the pipeline coverage value' do
expect(badge.status).to eq(40.00)
end
+
+ context 'with a more recent running pipeline' do
+ let!(:another_build) { create(:ci_build, :success, pipeline: running_pipeline, coverage: 20, created_at: 7.seconds.ago, name: 'coverage') }
+
+ it 'returns the running pipeline coverage value' do
+ expect(badge.status).to eq(20.00)
+ end
+ end
+
+ context 'with a more recent failed pipeline' do
+ let!(:another_build) { create(:ci_build, :success, pipeline: failure_pipeline, coverage: 10, created_at: 6.seconds.ago, name: 'coverage') }
+
+ it 'returns the failed pipeline coverage value' do
+ expect(badge.status).to eq(10.00)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 20e84bacd55..bc734644d29 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -203,16 +203,40 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#requests_remaining?' do
let(:client) { described_class.new('foo') }
- it 'returns true if enough requests remain' do
- expect(client).to receive(:remaining_requests).and_return(9000)
+ context 'when default requests limit is set' do
+ before do
+ allow(client).to receive(:requests_limit).and_return(5000)
+ end
- expect(client.requests_remaining?).to eq(true)
+ it 'returns true if enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(9000)
+
+ expect(client.requests_remaining?).to eq(true)
+ end
+
+ it 'returns false if not enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(1)
+
+ expect(client.requests_remaining?).to eq(false)
+ end
end
- it 'returns false if not enough requests remain' do
- expect(client).to receive(:remaining_requests).and_return(1)
+ context 'when search requests limit is set' do
+ before do
+ allow(client).to receive(:requests_limit).and_return(described_class::SEARCH_MAX_REQUESTS_PER_MINUTE)
+ end
- expect(client.requests_remaining?).to eq(false)
+ it 'returns true if enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(described_class::SEARCH_RATE_LIMIT_THRESHOLD + 1)
+
+ expect(client.requests_remaining?).to eq(true)
+ end
+
+ it 'returns false if not enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(described_class::SEARCH_RATE_LIMIT_THRESHOLD - 1)
+
+ expect(client.requests_remaining?).to eq(false)
+ end
end
end
@@ -262,6 +286,16 @@ RSpec.describe Gitlab::GithubImport::Client do
end
end
+ describe '#requests_limit' do
+ it 'returns requests limit' do
+ client = described_class.new('foo')
+ rate_limit = double(limit: 1)
+
+ expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
+ expect(client.requests_limit).to eq(1)
+ end
+ end
+
describe '#rate_limit_resets_in' do
it 'returns the number of seconds after which the rate limit is reset' do
client = described_class.new('foo')
@@ -417,4 +451,61 @@ RSpec.describe Gitlab::GithubImport::Client do
expect(client.rate_limiting_enabled?).to eq(false)
end
end
+
+ describe 'search' do
+ let(:client) { described_class.new('foo') }
+ let(:user) { double(:user, login: 'user') }
+ let(:org1) { double(:org, login: 'org1') }
+ let(:org2) { double(:org, login: 'org2') }
+ let(:repo1) { double(:repo, full_name: 'repo1') }
+ let(:repo2) { double(:repo, full_name: 'repo2') }
+
+ before do
+ allow(client)
+ .to receive(:each_object)
+ .with(:repos, nil, { affiliation: 'collaborator' })
+ .and_return([repo1, repo2].to_enum)
+
+ allow(client)
+ .to receive(:each_object)
+ .with(:organizations)
+ .and_return([org1, org2].to_enum)
+
+ allow(client.octokit).to receive(:user).and_return(user)
+ end
+
+ describe '#search_repos_by_name' do
+ it 'searches for repositories based on name' do
+ expected_search_query = 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2'
+
+ expect(client).to receive(:each_page).with(:search_repositories, expected_search_query)
+
+ client.search_repos_by_name('test')
+ end
+ end
+
+ describe '#search_query' do
+ it 'returns base search query' do
+ result = client.search_query(str: 'test', type: :test, include_collaborations: false, include_orgs: false)
+
+ expect(result).to eq('test in:test is:public,private user:user')
+ end
+
+ context 'when include_collaborations is true' do
+ it 'returns search query including users' do
+ result = client.search_query(str: 'test', type: :test, include_collaborations: true, include_orgs: false)
+
+ expect(result).to eq('test in:test is:public,private user:user repo:repo1 repo:repo2')
+ end
+ end
+
+ context 'when include_orgs is true' do
+ it 'returns search query including users' do
+ result = client.search_query(str: 'test', type: :test, include_collaborations: false, include_orgs: true)
+
+ expect(result).to eq('test in:test is:public,private user:user org:org1 org:org2')
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 9427fdfc0fe..f320b8a66e8 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -465,4 +465,34 @@ RSpec.describe Gitlab::PathRegex do
it_behaves_like 'invalid snippet routes'
end
+
+ describe '.container_image_regex' do
+ subject { described_class.container_image_regex }
+
+ it { is_expected.to match('gitlab-foss') }
+ it { is_expected.to match('gitlab_foss') }
+ it { is_expected.to match('gitlab-org/gitlab-foss') }
+ it { is_expected.to match('100px.com/100px.ruby') }
+
+ it 'only matches at most one slash' do
+ expect(subject.match('foo/bar/baz')[0]).to eq('foo/bar')
+ end
+
+ it 'does not match other non-word characters' do
+ expect(subject.match('ruby:2.7.0')[0]).to eq('ruby')
+ end
+ end
+
+ describe '.container_image_blob_sha_regex' do
+ subject { described_class.container_image_blob_sha_regex }
+
+ it { is_expected.to match('sha256:asdf1234567890ASDF') }
+ it { is_expected.to match('foo:123') }
+ it { is_expected.to match('a12bc3f590szp') }
+ it { is_expected.not_to match('') }
+
+ it 'does not match malicious characters' do
+ expect(subject.match('sha256:asdf1234%2f')[0]).to eq('sha256:asdf1234')
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 065e756ea28..1ca370dc950 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1972,6 +1972,32 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe '.latest_running_for_ref' do
+ include_context 'with some outdated pipelines'
+
+ let!(:latest_running_pipeline) do
+ create_pipeline(:running, 'ref', 'D', project)
+ end
+
+ it 'returns the latest running pipeline' do
+ expect(described_class.latest_running_for_ref('ref'))
+ .to eq(latest_running_pipeline)
+ end
+ end
+
+ describe '.latest_failed_for_ref' do
+ include_context 'with some outdated pipelines'
+
+ let!(:latest_failed_pipeline) do
+ create_pipeline(:failed, 'ref', 'D', project)
+ end
+
+ it 'returns the latest failed pipeline' do
+ expect(described_class.latest_failed_for_ref('ref'))
+ .to eq(latest_failed_pipeline)
+ end
+ end
+
describe '.latest_successful_for_sha' do
include_context 'with some outdated pipelines'
diff --git a/spec/models/dependency_proxy/blob_spec.rb b/spec/models/dependency_proxy/blob_spec.rb
new file mode 100644
index 00000000000..7c8a1eb95e8
--- /dev/null
+++ b/spec/models/dependency_proxy/blob_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::Blob, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:group) }
+ it { is_expected.to validate_presence_of(:file) }
+ it { is_expected.to validate_presence_of(:file_name) }
+ end
+
+ describe '.total_size' do
+ it 'returns 0 if no files' do
+ expect(described_class.total_size).to eq(0)
+ end
+
+ it 'returns a correct sum of all files sizes' do
+ create(:dependency_proxy_blob, size: 10)
+ create(:dependency_proxy_blob, size: 20)
+
+ expect(described_class.total_size).to eq(30)
+ end
+ end
+
+ describe '.find_or_build' do
+ let!(:blob) { create(:dependency_proxy_blob) }
+
+ it 'builds new instance if not found' do
+ expect(described_class.find_or_build('foo.gz')).not_to be_persisted
+ end
+
+ it 'finds an existing blob' do
+ expect(described_class.find_or_build(blob.file_name)).to eq(blob)
+ end
+ end
+
+ describe 'file is being stored' do
+ subject { create(:dependency_proxy_blob) }
+
+ context 'when existing object has local store' do
+ it_behaves_like 'mounted file in local store'
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_dependency_proxy_object_storage(direct_upload: true)
+ end
+
+ it_behaves_like 'mounted file in object store'
+ end
+ end
+end
diff --git a/spec/models/dependency_proxy/group_setting_spec.rb b/spec/models/dependency_proxy/group_setting_spec.rb
new file mode 100644
index 00000000000..c4c4a877d50
--- /dev/null
+++ b/spec/models/dependency_proxy/group_setting_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::GroupSetting, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:group) }
+ end
+end
diff --git a/spec/models/dependency_proxy/registry_spec.rb b/spec/models/dependency_proxy/registry_spec.rb
new file mode 100644
index 00000000000..5bfa75a2eed
--- /dev/null
+++ b/spec/models/dependency_proxy/registry_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::Registry, type: :model do
+ let(:tag) { '2.3.5-alpine' }
+ let(:blob_sha) { '40bd001563085fc35165329ea1ff5c5ecbdbbeef' }
+
+ context 'image name without namespace' do
+ let(:image) { 'ruby' }
+
+ describe '#auth_url' do
+ it 'returns a correct auth url' do
+ expect(described_class.auth_url(image))
+ .to eq('https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/ruby:pull')
+ end
+ end
+
+ describe '#manifest_url' do
+ it 'returns a correct manifest url' do
+ expect(described_class.manifest_url(image, tag))
+ .to eq('https://registry-1.docker.io/v2/library/ruby/manifests/2.3.5-alpine')
+ end
+ end
+
+ describe '#blob_url' do
+ it 'returns a correct blob url' do
+ expect(described_class.blob_url(image, blob_sha))
+ .to eq('https://registry-1.docker.io/v2/library/ruby/blobs/40bd001563085fc35165329ea1ff5c5ecbdbbeef')
+ end
+ end
+ end
+
+ context 'image name with namespace' do
+ let(:image) { 'foo/ruby' }
+
+ describe '#auth_url' do
+ it 'returns a correct auth url' do
+ expect(described_class.auth_url(image))
+ .to eq('https://auth.docker.io/token?service=registry.docker.io&scope=repository:foo/ruby:pull')
+ end
+ end
+
+ describe '#manifest_url' do
+ it 'returns a correct manifest url' do
+ expect(described_class.manifest_url(image, tag))
+ .to eq('https://registry-1.docker.io/v2/foo/ruby/manifests/2.3.5-alpine')
+ end
+ end
+
+ describe '#blob_url' do
+ it 'returns a correct blob url' do
+ expect(described_class.blob_url(image, blob_sha))
+ .to eq('https://registry-1.docker.io/v2/foo/ruby/blobs/40bd001563085fc35165329ea1ff5c5ecbdbbeef')
+ end
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 357238bb8b2..dd1faf999b3 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -28,6 +28,8 @@ RSpec.describe Group do
it { is_expected.to have_many(:iterations) }
it { is_expected.to have_many(:group_deploy_keys) }
it { is_expected.to have_many(:services) }
+ it { is_expected.to have_one(:dependency_proxy_setting) }
+ it { is_expected.to have_many(:dependency_proxy_blobs) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/requests/api/dependency_proxy_spec.rb b/spec/requests/api/dependency_proxy_spec.rb
new file mode 100644
index 00000000000..d59f2bf06e3
--- /dev/null
+++ b/spec/requests/api/dependency_proxy_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::DependencyProxy, api: true do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:blob) { create(:dependency_proxy_blob )}
+ let_it_be(:group, reload: true) { blob.group }
+
+ before do
+ group.add_owner(user)
+ stub_config(dependency_proxy: { enabled: true })
+ stub_last_activity_update
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ describe 'DELETE /groups/:id/dependency_proxy/cache' do
+ subject { delete api("/groups/#{group.id}/dependency_proxy/cache", user) }
+
+ context 'with feature available and enabled' do
+ let_it_be(:lease_key) { "dependency_proxy:delete_group_blobs:#{group.id}" }
+
+ context 'an admin user' do
+ it 'deletes the blobs and returns no content' do
+ stub_exclusive_lease(lease_key, timeout: 1.hour)
+ expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
+ it 'returns 409 with an error message' do
+ stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:conflict)
+ expect(response.body).to include('This request has already been made.')
+ end
+
+ it 'executes service only for the first time' do
+ expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async).once
+
+ 2.times { subject }
+ end
+ end
+ end
+
+ context 'a non-admin' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ end
+
+ it_behaves_like 'returning response status', :forbidden
+ end
+ end
+
+ context 'depencency proxy is not enabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: false })
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 9de99b73d23..f4d5ccc81b6 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -79,4 +79,40 @@ RSpec.describe "Groups", "routing" do
let(:group_path) { 'projects.abc123' }
end
end
+
+ describe 'dependency proxy for containers' do
+ context 'image name without namespace' do
+ it 'routes to #manifest' do
+ expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6'))
+ .to route_to('groups/dependency_proxy_for_containers#manifest', group_id: 'gitlabhq', image: 'ruby', tag: '2.3.6')
+ end
+
+ it 'routes to #blob' do
+ expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/blobs/abc12345'))
+ .to route_to('groups/dependency_proxy_for_containers#blob', group_id: 'gitlabhq', image: 'ruby', sha: 'abc12345')
+ end
+
+ it 'does not route to #blob with an invalid sha' do
+ expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/blobs/sha256:asdf1234%2f%2e%2e'))
+ .not_to route_to(group_id: 'gitlabhq', image: 'ruby', sha: 'sha256:asdf1234%2f%2e%2e')
+ end
+
+ it 'does not route to #blob with an invalid image' do
+ expect(get('/v2/gitlabhq/dependency_proxy/containers/ru*by/blobs/abc12345'))
+ .not_to route_to('groups/dependency_proxy_for_containers#blob', group_id: 'gitlabhq', image: 'ru*by', sha: 'abc12345')
+ end
+ end
+
+ context 'image name with namespace' do
+ it 'routes to #manifest' do
+ expect(get('/v2/gitlabhq/dependency_proxy/containers/foo/bar/manifests/2.3.6'))
+ .to route_to('groups/dependency_proxy_for_containers#manifest', group_id: 'gitlabhq', image: 'foo/bar', tag: '2.3.6')
+ end
+
+ it 'routes to #blob' do
+ expect(get('/v2/gitlabhq/dependency_proxy/containers/foo/bar/blobs/abc12345'))
+ .to route_to('groups/dependency_proxy_for_containers#blob', group_id: 'gitlabhq', image: 'foo/bar', sha: 'abc12345')
+ end
+ end
+ end
end
diff --git a/spec/services/dependency_proxy/download_blob_service_spec.rb b/spec/services/dependency_proxy/download_blob_service_spec.rb
new file mode 100644
index 00000000000..4b5c6b5bd6a
--- /dev/null
+++ b/spec/services/dependency_proxy/download_blob_service_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::DownloadBlobService do
+ include DependencyProxyHelpers
+
+ let(:image) { 'alpine' }
+ let(:token) { Digest::SHA256.hexdigest('123') }
+ let(:blob_sha) { Digest::SHA256.hexdigest('ruby:2.7.0') }
+
+ subject { described_class.new(image, blob_sha, token).execute }
+
+ context 'remote request is successful' do
+ before do
+ stub_blob_download(image, blob_sha)
+ end
+
+ it { expect(subject[:status]).to eq(:success) }
+ it { expect(subject[:file]).to be_a(Tempfile) }
+ it { expect(subject[:file].size).to eq(6) }
+ end
+
+ context 'remote request is not found' do
+ before do
+ stub_blob_download(image, blob_sha, 404)
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(404) }
+ it { expect(subject[:message]).to eq('Non-success response code on downloading blob fragment') }
+ end
+
+ context 'net timeout exception' do
+ before do
+ blob_url = DependencyProxy::Registry.blob_url(image, blob_sha)
+
+ stub_full_request(blob_url).to_timeout
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(599) }
+ it { expect(subject[:message]).to eq('execution expired') }
+ end
+end
diff --git a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
new file mode 100644
index 00000000000..4ba53d49d38
--- /dev/null
+++ b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::FindOrCreateBlobService do
+ include DependencyProxyHelpers
+
+ let(:blob) { create(:dependency_proxy_blob) }
+ let(:group) { blob.group }
+ let(:image) { 'alpine' }
+ let(:tag) { '3.9' }
+ let(:token) { Digest::SHA256.hexdigest('123') }
+ let(:blob_sha) { '40bd001563085fc35165329ea1ff5c5ecbdbbeef' }
+
+ subject { described_class.new(group, image, token, blob_sha).execute }
+
+ before do
+ stub_registry_auth(image, token)
+ end
+
+ context 'no cache' do
+ before do
+ stub_blob_download(image, blob_sha)
+ end
+
+ it 'downloads blob from remote registry if there is no cached one' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:blob]).to be_a(DependencyProxy::Blob)
+ expect(subject[:blob]).to be_persisted
+ end
+ end
+
+ context 'cached blob' do
+ let(:blob_sha) { blob.file_name.sub('.gz', '') }
+
+ it 'uses cached blob instead of downloading one' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:blob]).to be_a(DependencyProxy::Blob)
+ expect(subject[:blob]).to eq(blob)
+ end
+ end
+
+ context 'no such blob exists remotely' do
+ before do
+ stub_blob_download(image, blob_sha, 404)
+ end
+
+ it 'returns error message and http status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Failed to download the blob')
+ expect(subject[:http_status]).to eq(404)
+ end
+ end
+end
diff --git a/spec/services/dependency_proxy/pull_manifest_service_spec.rb b/spec/services/dependency_proxy/pull_manifest_service_spec.rb
new file mode 100644
index 00000000000..030ed9c001b
--- /dev/null
+++ b/spec/services/dependency_proxy/pull_manifest_service_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::PullManifestService do
+ include DependencyProxyHelpers
+
+ let(:image) { 'alpine' }
+ let(:tag) { '3.9' }
+ let(:token) { Digest::SHA256.hexdigest('123') }
+ let(:manifest) { { foo: 'bar' }.to_json }
+
+ subject { described_class.new(image, tag, token).execute }
+
+ context 'remote request is successful' do
+ before do
+ stub_manifest_download(image, tag)
+ end
+
+ it { expect(subject[:status]).to eq(:success) }
+ it { expect(subject[:manifest]).to eq(manifest) }
+ end
+
+ context 'remote request is not found' do
+ before do
+ stub_manifest_download(image, tag, 404, 'Not found')
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(404) }
+ it { expect(subject[:message]).to eq('Not found') }
+ end
+
+ context 'net timeout exception' do
+ before do
+ manifest_link = DependencyProxy::Registry.manifest_url(image, tag)
+
+ stub_full_request(manifest_link).to_timeout
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(599) }
+ it { expect(subject[:message]).to eq('execution expired') }
+ end
+end
diff --git a/spec/services/dependency_proxy/request_token_service_spec.rb b/spec/services/dependency_proxy/request_token_service_spec.rb
new file mode 100644
index 00000000000..8b3ba783b8d
--- /dev/null
+++ b/spec/services/dependency_proxy/request_token_service_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::RequestTokenService do
+ include DependencyProxyHelpers
+
+ let(:image) { 'alpine:3.9' }
+ let(:token) { Digest::SHA256.hexdigest('123') }
+
+ subject { described_class.new(image).execute }
+
+ context 'remote request is successful' do
+ before do
+ stub_registry_auth(image, token)
+ end
+
+ it { expect(subject[:status]).to eq(:success) }
+ it { expect(subject[:token]).to eq(token) }
+ end
+
+ context 'remote request is not found' do
+ before do
+ stub_registry_auth(image, token, 404)
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(404) }
+ it { expect(subject[:message]).to eq('Expected 200 response code for an access token') }
+ end
+
+ context 'failed to parse response body' do
+ before do
+ stub_registry_auth(image, token, 200, 'dasd1321: wow')
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(500) }
+ it { expect(subject[:message]).to eq('Failed to parse a response body for an access token') }
+ end
+
+ context 'net timeout exception' do
+ before do
+ auth_link = DependencyProxy::Registry.auth_url(image)
+
+ stub_full_request(auth_link, method: :any).to_timeout
+ end
+
+ it { expect(subject[:status]).to eq(:error) }
+ it { expect(subject[:http_status]).to eq(599) }
+ it { expect(subject[:message]).to eq('execution expired') }
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 65ab071f179..38e3f851116 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -56,6 +56,7 @@ require_relative('../ee/spec/spec_helper') if Gitlab.ee?
# Load these first since they may be required by other helpers
require Rails.root.join("spec/support/helpers/git_helpers.rb")
+require Rails.root.join("spec/support/helpers/stub_requests.rb")
# Then the rest
Dir[Rails.root.join("spec/support/helpers/*.rb")].sort.each { |f| require f }
diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb
new file mode 100644
index 00000000000..545b9d1f4d0
--- /dev/null
+++ b/spec/support/helpers/dependency_proxy_helpers.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module DependencyProxyHelpers
+ include StubRequests
+
+ def stub_registry_auth(image, token, status = 200, body = nil)
+ auth_body = { 'token' => token }.to_json
+ auth_link = registry.auth_url(image)
+
+ stub_full_request(auth_link)
+ .to_return(status: status, body: body || auth_body)
+ end
+
+ def stub_manifest_download(image, tag, status = 200, body = nil)
+ manifest_url = registry.manifest_url(image, tag)
+
+ stub_full_request(manifest_url)
+ .to_return(status: status, body: body || manifest)
+ end
+
+ def stub_blob_download(image, blob_sha, status = 200, body = '123456')
+ download_link = registry.blob_url(image, blob_sha)
+
+ stub_full_request(download_link)
+ .to_return(status: status, body: body)
+ end
+
+ private
+
+ def registry
+ @registry ||= DependencyProxy::Registry
+ end
+end
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index 11e67ba394c..e18a708e41c 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -36,4 +36,12 @@ module NavbarStructureHelper
new_sub_nav_item_name: _('Container Registry')
)
end
+
+ def insert_dependency_proxy_nav(within)
+ insert_after_sub_nav_item(
+ _('Package Registry'),
+ within: _('Packages & Registries'),
+ new_sub_nav_item_name: _('Dependency Proxy')
+ )
+ end
end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 93f58a6ca0c..5a4322f73b6 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -145,6 +145,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
group.add_owner(user)
client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo])
allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum)
+ # GitHub controller has filtering done using GitHub Search API
+ stub_feature_flags(remove_legacy_github_client: false)
end
it 'filters list of repositories by name' do
diff --git a/spec/uploaders/dependency_proxy/file_uploader_spec.rb b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
new file mode 100644
index 00000000000..724a9c42f47
--- /dev/null
+++ b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::FileUploader do
+ let(:blob) { create(:dependency_proxy_blob) }
+ let(:uploader) { described_class.new(blob, :file) }
+ let(:path) { Gitlab.config.dependency_proxy.storage_path }
+
+ subject { uploader }
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}],
+ cache_dir: %r[/dependency_proxy/tmp/cache],
+ work_dir: %r[/dependency_proxy/tmp/work]
+
+ context 'object store is remote' do
+ before do
+ stub_dependency_proxy_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}]
+ end
+end
diff --git a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
new file mode 100644
index 00000000000..9cd3b6636f5
--- /dev/null
+++ b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PurgeDependencyProxyCacheWorker do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:blob) { create(:dependency_proxy_blob )}
+ let_it_be(:group, reload: true) { blob.group }
+ let_it_be(:group_id) { group.id }
+
+ subject { described_class.new.perform(user.id, group_id) }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ describe '#perform' do
+ shared_examples 'returns nil' do
+ it 'returns nil' do
+ expect { subject }.not_to change { group.dependency_proxy_blobs.size }
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'an admin user' do
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [user.id, group_id] }
+
+ it 'deletes the blobs and returns ok' do
+ expect(group.dependency_proxy_blobs.size).to eq(1)
+
+ subject
+
+ expect(group.dependency_proxy_blobs.size).to eq(0)
+ end
+ end
+ end
+
+ context 'a non-admin user' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'returns nil'
+ end
+
+ context 'an invalid user id' do
+ let(:user) { double('User', id: 99999 ) }
+
+ it_behaves_like 'returns nil'
+ end
+
+ context 'an invalid group' do
+ let(:group_id) { 99999 }
+
+ it_behaves_like 'returns nil'
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index a18b1f9beaf..91f08a59712 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -845,10 +845,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.5.tgz#5f6bfe6baaef360daa9b038fa78798d7a6a916b4"
integrity sha512-282Dn3SPVsUHVDhMsXgfnv+Rzog0uxecjttxGRQvxh25es1+xvkGQFsvJfkSKJ3X1kHVkSjKf+Tt5Rra+Jhp9g==
-"@gitlab/eslint-plugin@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-4.1.0.tgz#b59b53b9cd984dc692cd094cca4fbe45e466c8b7"
- integrity sha512-NjY8XqT9lPKxBiKeaIXAGPGuxLMmbns/W5YF/cqxfWMnMOCCGBPZjwwnPY5wzAeKocT014LCnA7k0ztIUN7DoQ==
+"@gitlab/eslint-plugin@5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-5.0.0.tgz#502eb2bccb55d65d6310ce9ef2da76035b6fc319"
+ integrity sha512-zd4pa6D2OUuhPUD2QmyhfpZh7vuXKsNHaCHJdbTb2ld+mHel5IDqdidAzAshCI9On3e6o9XieD6l7rBTpN6H/g==
dependencies:
babel-eslint "^10.0.3"
eslint-config-airbnb-base "^14.0.0"