Commit 1818c1ac authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '5934-npm-package-management-mvc' into 'master'

Add NPM registry support to GitLab packages

Closes #5934

See merge request gitlab-org/gitlab-ee!8673
parents d8bfaefd 05db2da6
...@@ -128,7 +128,8 @@ created in snippets, wikis, and repos. ...@@ -128,7 +128,8 @@ created in snippets, wikis, and repos.
- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project. - [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet. - [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet.
- [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]** - [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
- [Maven Repository](maven_repository.md): Enable Maven Repository within GitLab. - [Maven Repository](maven_repository.md): Enable Maven Repository within GitLab. **[PREMIUM ONLY]**
- [NPM Registry](npm_registry.md): Enable NPM Registry within GitLab. **[PREMIUM ONLY]**
### Repository settings ### Repository settings
......
...@@ -16,153 +16,4 @@ Once enabled, newly created projects will have the Packages feature enabled by ...@@ -16,153 +16,4 @@ Once enabled, newly created projects will have the Packages feature enabled by
default. Existing projects will need to default. Existing projects will need to
[explicitly enabled it](../user/project/packages/maven_repository.md#enabling-the-packages-repository). [explicitly enabled it](../user/project/packages/maven_repository.md#enabling-the-packages-repository).
To enable the Maven repository: To enable the Maven repository you need to enable [Packages feature](packages.md)
**Omnibus GitLab installations**
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
```ruby
gitlab_rails['packages_enabled'] = true
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. After the installation is complete, you will have to configure the `packages`
section in `config/gitlab.yml`. Set to `true` to enable it:
```yaml
packages:
enabled: true
```
1. [Restart GitLab] for the changes to take effect.
## Changing the storage path
By default, the packages are stored locally, but you can change the default
local location or even use object storage.
### Changing the local storage path
The packages for Omnibus GitLab installations are stored under
`/var/opt/gitlab/gitlab-rails/shared/packages/` and for source
installations under `shared/packages/` (relative to the git homedir).
To change the local storage path:
**Omnibus GitLab installations**
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
```ruby
gitlab_rails['packages_storage_path'] = "/mnt/maven"
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. Edit the `packages` section in `config/gitlab.yml`:
```yaml
packages:
enabled: true
storage_path: shared/packages
```
1. [Restart GitLab] for the changes to take effect.
### Using object storage
Instead of relying on the local storage, you can use an object storage to
upload the maven packages:
**Omnibus GitLab installations**
1. Edit `/etc/gitlab/gitlab.rb` and add the following lines (uncomment where
necessary):
```ruby
gitlab_rails['packages_enabled'] = true
gitlab_rails['packages_storage_path'] = "/var/opt/gitlab/gitlab-rails/shared/packages"
gitlab_rails['packages_object_store_enabled'] = true
gitlab_rails['packages_object_store_remote_directory'] = "packages" # The bucket name.
gitlab_rails['packages_object_store_direct_upload'] = false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
gitlab_rails['packages_object_store_background_upload'] = true # Temporary option to limit automatic upload (Default: true).
gitlab_rails['packages_object_store_proxy_download'] = false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
gitlab_rails['packages_object_store_connection'] = {
##
## If the provider is AWS S3, uncomment the following
##
#'provider' => 'AWS',
#'region' => 'eu-west-1',
#'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
#'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY',
##
## If the provider is other than AWS (an S3-compatible one), uncomment the following
##
#'host' => 's3.amazonaws.com',
#'aws_signature_version' => 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
#'endpoint' => 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
#'path_style' => false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
}
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. Edit the `packages` section in `config/gitlab.yml` (uncomment where necessary):
```yaml
packages:
enabled: true
##
## The location where build packages are stored (default: shared/packages).
##
#storage_path: shared/packages
object_store:
enabled: false
remote_directory: packages # The bucket name.
#direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
#background_upload: true # Temporary option to limit automatic upload (Default: true).
#proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
connection:
##
## If the provider is AWS S3, uncomment the following
##
#provider: AWS
#region: us-east-1
#aws_access_key_id: AWS_ACCESS_KEY_ID
#aws_secret_access_key: AWS_SECRET_ACCESS_KEY
##
## If the provider is other than AWS (an S3-compatible one), uncomment the following
##
#host: 's3.amazonaws.com' # default: s3.amazonaws.com.
#aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
#endpoint: 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
#path_style: false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
```
1. [Restart GitLab] for the changes to take effect.
### Migrating local packages to object storage
After [configuring the object storage](#using-object-storage), you may use the
following task to migrate existing packages from the local storage to the remote one.
The processing will be done in a background worker and requires **no downtime**.
For Omnibus GitLab:
```sh
sudo gitlab-rake "gitlab:packages:migrate"
```
For installations from source:
```bash
RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:packages:migrate
```
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
# GitLab NPM Registry administration **[PREMIUM ONLY]**
>
[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5934)
in GitLab 11.7. To learn how to use
When the GitLab NPM Registry is enabled, every project in GitLab will have
its own space to store [NPM](https://www.npmjs.com/) packages.
To learn how to use it, see the [user documentation](../user/project/packages/npm_registry.md).
## Enabling the NPM Registry
NOTE: **Note:**
Once enabled, newly created projects will have the Packages feature enabled by
default. Existing projects will need to
[explicitly enabled it](../user/project/packages/npm_registry.md#enabling-the-packages-repository).
To enable the NPM Registry you need to enable [Packages feature](packages.md)
# GitLab Packages administration **[PREMIUM ONLY]**
Package feature provides a support for next products:
* [Maven Repository](maven_repository.md)
* [NPM Registry](npm_registry.md)
## Enabling the Packages feature
To enable the Packages feature:
**Omnibus GitLab installations**
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
```ruby
gitlab_rails['packages_enabled'] = true
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. After the installation is complete, you will have to configure the `packages`
section in `config/gitlab.yml`. Set to `true` to enable it:
```yaml
packages:
enabled: true
```
1. [Restart GitLab] for the changes to take effect.
## Changing the storage path
By default, the packages are stored locally, but you can change the default
local location or even use object storage.
### Changing the local storage path
The packages for Omnibus GitLab installations are stored under
`/var/opt/gitlab/gitlab-rails/shared/packages/` and for source
installations under `shared/packages/` (relative to the git homedir).
To change the local storage path:
**Omnibus GitLab installations**
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
```ruby
gitlab_rails['packages_storage_path'] = "/mnt/packages"
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. Edit the `packages` section in `config/gitlab.yml`:
```yaml
packages:
enabled: true
storage_path: shared/packages
```
1. [Restart GitLab] for the changes to take effect.
### Using object storage
Instead of relying on the local storage, you can use an object storage to
upload packages:
**Omnibus GitLab installations**
1. Edit `/etc/gitlab/gitlab.rb` and add the following lines (uncomment where
necessary):
```ruby
gitlab_rails['packages_enabled'] = true
gitlab_rails['packages_storage_path'] = "/var/opt/gitlab/gitlab-rails/shared/packages"
gitlab_rails['packages_object_store_enabled'] = true
gitlab_rails['packages_object_store_remote_directory'] = "packages" # The bucket name.
gitlab_rails['packages_object_store_direct_upload'] = false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
gitlab_rails['packages_object_store_background_upload'] = true # Temporary option to limit automatic upload (Default: true).
gitlab_rails['packages_object_store_proxy_download'] = false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
gitlab_rails['packages_object_store_connection'] = {
##
## If the provider is AWS S3, uncomment the following
##
#'provider' => 'AWS',
#'region' => 'eu-west-1',
#'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
#'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY',
##
## If the provider is other than AWS (an S3-compatible one), uncomment the following
##
#'host' => 's3.amazonaws.com',
#'aws_signature_version' => 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
#'endpoint' => 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
#'path_style' => false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
}
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. Edit the `packages` section in `config/gitlab.yml` (uncomment where necessary):
```yaml
packages:
enabled: true
##
## The location where build packages are stored (default: shared/packages).
##
#storage_path: shared/packages
object_store:
enabled: false
remote_directory: packages # The bucket name.
#direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
#background_upload: true # Temporary option to limit automatic upload (Default: true).
#proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
connection:
##
## If the provider is AWS S3, uncomment the following
##
#provider: AWS
#region: us-east-1
#aws_access_key_id: AWS_ACCESS_KEY_ID
#aws_secret_access_key: AWS_SECRET_ACCESS_KEY
##
## If the provider is other than AWS (an S3-compatible one), uncomment the following
##
#host: 's3.amazonaws.com' # default: s3.amazonaws.com.
#aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
#endpoint: 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
#path_style: false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
```
1. [Restart GitLab] for the changes to take effect.
### Migrating local packages to object storage
After [configuring the object storage](#using-object-storage), you may use the
following task to migrate existing packages from the local storage to the remote one.
The processing will be done in a background worker and requires **no downtime**.
For Omnibus GitLab:
```sh
sudo gitlab-rake "gitlab:packages:migrate"
```
For installations from source:
```bash
RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:packages:migrate
```
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
...@@ -89,6 +89,7 @@ website with GitLab Pages ...@@ -89,6 +89,7 @@ website with GitLab Pages
your code blocks, overriding GitLab's default choice of language your code blocks, overriding GitLab's default choice of language
- [Badges](badges.md): Badges for the project overview - [Badges](badges.md): Badges for the project overview
- [Maven packages](packages/maven_repository.md): Your private Maven repository in GitLab **[PREMIUM]** - [Maven packages](packages/maven_repository.md): Your private Maven repository in GitLab **[PREMIUM]**
- [NPM packages](packages/npm_registry.md): Your private NPM package registry in GitLab **[PREMIUM]**
- [Code owners](code_owners.md): Specify code owners for certain files **[STARTER]** - [Code owners](code_owners.md): Specify code owners for certain files **[STARTER]**
- [License Management](merge_requests/license_management.md): Approve and blacklist licenses for projects **[ULTIMATE]** - [License Management](merge_requests/license_management.md): Approve and blacklist licenses for projects **[ULTIMATE]**
......
# GitLab NPM Registry **[PREMIUM]**
> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
With the GitLab NPM Registry, every
project can have its own space to store NPM packages.
![GitLab NPM Registry](img/npm_package_view.png)
NOTE: **Note:**
Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported.
## Enabling NPM Registry
NOTE: **Note:**
This option is available only if your GitLab administrator has
[enabled Packages feature](../../../administration/npm_registry.md).
In order to use the GitLab NPM Registry, you must enable the Packages feature.
To enable (or disable) it:
1. Navigate to your project's **Settings > General > Permissions**.
1. Find the "Packages" feature and enable it.
1. Click on **Save changes** for the changes to take effect.
You should then be able to see the **Packages** section on the left sidebar.
Next, you must configure your project to authorize with the GitLab NPM
registry.
## Package naming convention
Note that **only packages that have the same path as the project** are supported.
| Project | Package | Supported |
| ------- | ------- | --------------------------------- |
| `foo/bar` | `@foo/bar` | Yes |
| `gitlab-org/gitlab-ce` | `@foo/bar` | No |
| `gitlab-org/gitlab-ce` | `@gitlab-org/gitlab-ce` | Yes |
## Authenticating to the GitLab NPM Registry
If a project is private or you want to upload NPM package to GitLab,
credentials will need to be provided for authentication. Support is available for
[oauth tokens](#authenticating-with-an-oauth-token) only.
### Authenticating with an oauth token
To authenticate with a [oauth token](../../../api/oauth2.md),
add a corresponding section to your `.npmrc` file:
```
; Set URL for your scoped packages.
; For example package with name `@foo/bar` will use this URL for download
@foo:registry=https://gitlab.com/api/v4/packages/npm/
; Add OAuth token for scoped packages URL. This will allow you to download
; `@foo/` packages from private projects.
//localhost:3001/api/v4/packages/npm/:_authToken=1da4f6691c92a543f7416b8fe013357fda23b0730466841311b89809a51349ce
; Add OAuth token for uploading to the registry. Replace `YOUR_PROJECT_ID`
; with a project you want your package uploaded to.
//gitlab.com/api/v4/projects/YOUR_PROJECT_ID/packages/npm/:_authToken=YOUR_OAUTH_TOKEN
```
You should now be able to download and upload NPM packages to your project.
## Uploading packages
Before you will be able to upload a package, you need to specify registry for NPM.
To do this, you need to add next section to the bottom of `package.json`:
```json
"publishConfig": {
"@foo:registry":"https://gitlab.com/api/v4/projects/YOUR_PROJECT_ID/packages/npm/"
}
```
Replace `YOUR_PROJECT_ID` with a project you want your package uploaded to.
And replace `@foo` with your own scope.
Once you did it and have set up the [authorization](#authorizing-with-the-gitlab-npm-registry),
test to upload an NPM package from a project of yours:
```sh
npm publish
```
You can then navigate to your project's **Packages** page and see the uploaded
packages or even delete them.
## Uploading a package with the same version twice
If you upload a package with a same name and version twice, GitLab will show
both packages in UI. But API will expose only one package per version for `npm install`
and it will be the most recent one.
# frozen_string_literal: true
class Packages::NpmPackagesFinder
attr_reader :project, :package_name
def initialize(project, package_name)
@project = project
@package_name = package_name
end
def execute
packages
end
private
def packages
project.packages
.npm
.with_name(package_name)
.last_of_each_version
.preload_files
end
end
...@@ -12,7 +12,11 @@ class Packages::Package < ActiveRecord::Base ...@@ -12,7 +12,11 @@ class Packages::Package < ActiveRecord::Base
presence: true, presence: true,
format: { with: Gitlab::Regex.package_name_regex } format: { with: Gitlab::Regex.package_name_regex }
enum package_type: { maven: 1 } enum package_type: { maven: 1, npm: 2 }
scope :with_name, ->(name) { where(name: name) }
scope :preload_files, -> { preload(:package_files) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
def self.for_projects(projects) def self.for_projects(projects)
return none unless projects.any? return none unless projects.any?
...@@ -23,4 +27,10 @@ class Packages::Package < ActiveRecord::Base ...@@ -23,4 +27,10 @@ class Packages::Package < ActiveRecord::Base
def self.only_maven_packages_with_path(path) def self.only_maven_packages_with_path(path)
joins(:maven_metadatum).where(packages_maven_metadata: { path: path }) joins(:maven_metadatum).where(packages_maven_metadata: { path: path })
end end
def self.by_name_and_file_name(name, file_name)
with_name(name)
.joins(:package_files)
.where(packages_package_files: { file_name: file_name }).last!
end
end end
# frozen_string_literal: true
class NpmPackagePresenter
include API::Helpers::RelatedResourcesHelpers
attr_reader :project, :name, :packages
def initialize(project, name, packages)
@project = project
@name = name
@packages = packages
end
def versions
package_versions = {}
packages.each do |package|
package_file = package.package_files.last
next unless package_file
package_versions[package.version] = build_package_version(package, package_file)
end
package_versions
end
private
def build_package_version(package, package_file)
{
"name": package.name,
"version": package.version,
"dist": {
"shasum": package_file.file_sha1,
"tarball": tarball_url(package, package_file)
}
}
end
def tarball_url(package, package_file)
expose_url "#{api_v4_projects_path(id: package.project_id)}" \
"/packages/npm/#{package.name}" \
"/-/#{package_file.file_name}"
end
end
# frozen_string_literal: true
module Packages
class CreateNpmPackageService < BaseService
def execute
name = params[:name]
version = params[:versions].keys.first
version_data = params[:versions][version]
package = project.packages.create!(
name: name,
version: version,
package_type: 'npm'
)
package_file_name = "#{name}-#{version}.tgz"
attachment = params['_attachments'][package_file_name]
file_params = {
file: CarrierWaveStringFile.new(Base64.decode64(attachment['data'])),
size: attachment['length'],
file_sha1: version_data[:dist][:shasum],
file_name: package_file_name
}
::Packages::CreatePackageFileService.new(package, file_params).execute
package
end
end
end
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
.table-section.section-20 .table-section.section-20
.table-mobile-header{ role: "rowheader" }= _("Type") .table-mobile-header{ role: "rowheader" }= _("Type")
.table-mobile-content .table-mobile-content
= _('Maven package') = package.package_type
.table-section.section-20 .table-section.section-20
.table-mobile-header{ role: "rowheader" }= _("Created") .table-mobile-header{ role: "rowheader" }= _("Created")
.table-mobile-content .table-mobile-content
......
---
title: Add NPM registry support to GitLab packages
merge_request: 8673
author:
type: added
# frozen_string_literal: true
module API
module Helpers
module PackagesHelpers
def require_packages_enabled!
not_found! unless ::Gitlab.config.packages.enabled
end
def authorize_packages_feature!
forbidden! unless user_project.feature_available?(:packages)
end
def authorize_download_package!
authorize!(:read_package, user_project)
end
def authorize_create_package!
authorize!(:create_package, user_project)
end
end
end
end
...@@ -14,23 +14,9 @@ module API ...@@ -14,23 +14,9 @@ module API
authenticate_non_get! authenticate_non_get!
end end
helpers do helpers ::API::Helpers::PackagesHelpers
def require_packages_enabled!
not_found! unless Gitlab.config.packages.enabled
end
def authorize_packages_feature!
forbidden! unless user_project.feature_available?(:packages)
end
def authorize_download_package!
authorize!(:read_package, user_project)
end
def authorize_create_package!
authorize!(:create_package, user_project)
end
helpers do
def extract_format(file_name) def extract_format(file_name)
name, _, format = file_name.rpartition('.') name, _, format = file_name.rpartition('.')
......
# frozen_string_literal: true
module API
class NpmPackages < Grape::API
NPM_ENDPOINT_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
before do
require_packages_enabled!
authenticate_non_get!
end
helpers ::API::Helpers::PackagesHelpers
helpers do
def find_project_by_package_name(name)
Project.find_by_full_path(name.sub('@', ''))
end
def project_package_name_match?
"@#{user_project.full_path}" == params[:package_name]
end
def ensure_project_package_match!
bad_request!(:package_name) unless project_package_name_match?
end
end
desc 'NPM registry endpoint at instance level' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
end
get 'packages/npm/*package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
# To avoid name collision we require project path and project package be the same.
project = find_project_by_package_name(package_name)
authorize!(:read_package, project)
forbidden! unless project.feature_available?(:packages)
packages = ::Packages::NpmPackagesFinder
.new(project, package_name).execute
present NpmPackagePresenter.new(project, package_name, packages),
with: EE::API::Entities::NpmPackage
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
authorize_packages_feature!
ensure_project_package_match!
end
desc 'Download the NPM tarball' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :file_name, type: String, desc: 'Package file name'
end
get ':id/packages/npm/*package_name/-/*file_name', format: false do
authorize_download_package!
package = user_project.packages.npm
.by_name_and_file_name(params[:package_name], params[:file_name])
package_file = ::Packages::PackageFileFinder
.new(package, params[:file_name]).execute!
present_carrierwave_file!(package_file.file)
end
desc 'Create NPM package' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :versions, type: Hash, desc: 'Package version info'
end
put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do
authorize_create_package!
::Packages::CreateNpmPackageService
.new(user_project, current_user, params).execute
end
end
end
end
...@@ -21,6 +21,7 @@ module EE ...@@ -21,6 +21,7 @@ module EE
mount ::API::ProjectMirror mount ::API::ProjectMirror
mount ::API::ProjectPushRule mount ::API::ProjectPushRule
mount ::API::MavenPackages mount ::API::MavenPackages
mount ::API::NpmPackages
end end
end end
end end
......
...@@ -494,6 +494,11 @@ module EE ...@@ -494,6 +494,11 @@ module EE
expose :trial_ends_on expose :trial_ends_on
end end
end end
class NpmPackage < Grape::Entity
expose :name
expose :versions
end
end end
end end
end end
...@@ -16,7 +16,7 @@ module EE ...@@ -16,7 +16,7 @@ module EE
end end
def package_name_regex def package_name_regex
@package_name_regex ||= %r{\A(([\w\-\.]*)/)*([\w\-\.]*)\z}.freeze @package_name_regex ||= %r{\A\@?(([\w\-\.]*)/)*([\w\-\.]*)\z}.freeze
end end
def maven_path_regex def maven_path_regex
......
...@@ -19,6 +19,16 @@ FactoryBot.define do ...@@ -19,6 +19,16 @@ FactoryBot.define do
create :package_file, :pom, package: package create :package_file, :pom, package: package
end end
end end
factory :npm_package do
name 'foo'
version '1.0.0'
package_type 'npm'
after :create do |package|
create :package_file, :npm, package: package
end
end
end end
factory :package_file, class: Packages::PackageFile do factory :package_file, class: Packages::PackageFile do
...@@ -44,6 +54,17 @@ FactoryBot.define do ...@@ -44,6 +54,17 @@ FactoryBot.define do
file_sha1 '42b1bdc80de64953b6876f5a8c644f20204011b0' file_sha1 '42b1bdc80de64953b6876f5a8c644f20204011b0'
file_type 'xml' file_type 'xml'
end end
trait(:npm) do
file { fixture_file_upload('ee/spec/fixtures/npm/foo-1.0.1.tgz') }
file_name 'foo-1.0.1.tgz'
file_sha1 'be93151dc23ac34a82752444556fe79b32c7a1ad'
file_type 'tgz'
end
trait :object_storage do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end
end end
factory :maven_metadatum, class: Packages::MavenMetadatum do factory :maven_metadatum, class: Packages::MavenMetadatum do
......
# frozen_string_literal: true
require 'spec_helper'
describe Packages::NpmPackagesFinder do
let(:package) { create(:npm_package) }
let(:project) { package.project }
describe '#execute!' do
it 'returns project packages' do
finder = described_class.new(project, package.name)
expect(finder.execute).to eq([package])
end
it 'returns an empty collection' do
finder = described_class.new(project, 'baz')
expect(finder.execute).to be_empty
end
end
end
{
"type": "object",
"required" : ["name", "versions"],
"properties" : {
"name": { "type": "string" },
"versions": { "type": "object" }
}
}
{
"type": "object",
"required": ["name", "version", "dist"],
"properties" : {
"name": { "type": "string" },
"version": { "type": "string" },
"dist": {
"type": "object",
"required": ["shasum", "tarball"],
"properties" : {
"shasum": { "type": "string" },
"tarball": { "type": "string" }
}
}
}
}
...@@ -19,4 +19,18 @@ describe Gitlab::Regex do ...@@ -19,4 +19,18 @@ describe Gitlab::Regex do
it { is_expected.not_to match('my feature flag') } it { is_expected.not_to match('my feature flag') }
it { is_expected.not_to match('!!()()') } it { is_expected.not_to match('!!()()') }
end end
describe '.package_name_regex' do
subject { described_class.package_name_regex }
it { is_expected.to match('123') }
it { is_expected.to match('foo') }
it { is_expected.to match('foo/bar') }
it { is_expected.to match('@foo/bar') }
it { is_expected.to match('com/mycompany/app/my-app') }
it { is_expected.not_to match('$foo/bar') }
it { is_expected.not_to match('@foo/@/bar') }
it { is_expected.not_to match('my package name') }
it { is_expected.not_to match('!!()()') }
end
end end
...@@ -16,4 +16,32 @@ RSpec.describe Packages::Package, type: :model do ...@@ -16,4 +16,32 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) } it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) }
end end
end end
describe '.by_name_and_file_name' do
let!(:package) { create(:npm_package) }
let!(:package_file) { package.package_files.first }
subject { described_class }
it 'finds a package with correct arguiments' do
expect(subject.by_name_and_file_name(package.name, package_file.file_name)).to eq(package)
end
it 'will raise error if not found' do
expect { subject.by_name_and_file_name('foo', 'foo-5.5.5.tgz') }.to raise_error(ActiveRecord::RecordNotFound)
end
end
describe '.last_of_each_version' do
let!(:package1) { create(:npm_package, version: '1.0.0') }
let!(:package2) { create(:npm_package, version: '1.0.1') }
let!(:package3) { create(:npm_package, version: '1.0.1') }
subject { described_class.last_of_each_version }
it 'includes only latest package per version' do
is_expected.to include(package1, package3)
is_expected.not_to include(package2)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe API::NpmPackages do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:project) { create(:project, :public, namespace: group) }
let(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
before do
project.add_developer(user)
stub_licensed_features(packages: true)
end
shared_examples 'a package that requires auth' do
it 'returns the package info with oauth token' do
get_package_with_token(package)
expect_a_valid_package_response
end
it 'denies request without oauth token' do
get_package(package)
expect(response).to have_gitlab_http_status(403)
end
end
describe 'GET /api/v4/packages/npm/*package_name' do
let(:package) { create(:npm_package, project: project, name: "@#{project.full_path}") }
context 'a public project' do
it 'returns the package info without oauth token' do
get_package(package)
expect_a_valid_package_response
end
end
context 'internal project' do
before do
project.team.truncate
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it_behaves_like 'a package that requires auth'
end
context 'private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it_behaves_like 'a package that requires auth'
it 'denies request when not enough permissions' do
project.add_guest(user)
get_package_with_token(package)
expect(response).to have_gitlab_http_status(403)
end
end
it 'rejects request if feature is not in the license' do
stub_licensed_features(packages: false)
get_package(package)
expect(response).to have_gitlab_http_status(403)
end
context 'project name is different from a package name' do
let(:package) { create(:npm_package, project: project) }
it 'rejects request' do
get_package(package)
expect(response).to have_gitlab_http_status(403)
end
end
def get_package(package, params = {})
get api("/packages/npm/#{package.name}"), params
end
def get_package_with_token(package, params = {})
get_package(package, params.merge(access_token: token.token))
end
end
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
let(:package) { create(:npm_package, project: project, name: "@#{project.full_path}") }
let(:package_file) { package.package_files.first }
context 'a public project' do
it 'returns the file' do
get_file_with_token(package_file)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
context 'private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'returns the file' do
get_file_with_token(package_file)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when not enough permissions' do
project.add_guest(user)
get_file_with_token(package_file)
expect(response).to have_gitlab_http_status(403)
end
it 'denies download when no private token' do
get_file(package_file)
expect(response).to have_gitlab_http_status(404)
end
end
it 'rejects request if feature is not in the license' do
stub_licensed_features(packages: false)
get_file(package_file)
expect(response).to have_gitlab_http_status(403)
end
def get_file(package_file, params = {})
get api("/projects/#{project.id}/packages/npm/" \
"#{package_file.package.name}/-/#{package_file.file_name}"), params
end
def get_file_with_token(package_file, params = {})
get_file(package_file, params.merge(access_token: token.token))
end
end
describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
context 'when params are correct' do
context 'unscoped package' do
let(:package_name) { project.path }
let(:params) { upload_params(package_name) }
it 'denies the request with 400 error' do
expect { upload_package_with_token(package_name, params) }
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(400)
end
end
context 'scoped package' do
let(:package_name) { "@#{project.full_path}" }
let(:params) { upload_params(package_name) }
it 'creates npm package with file' do
expect { upload_package_with_token(package_name, params) }
.to change { project.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
end
end
def upload_package(package_name, params = {})
put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params
end
def upload_package_with_token(package_name, params = {})
upload_package(package_name, params.merge(access_token: token.token))
end
def upload_params(package_name)
{
name: package_name,
versions: {
'1.0.1' => {
dist: {
shasum: 'f572d396fae9206628714fb2ce00f72e94f2258f'
}
}
},
'_attachments' => {
"#{package_name}-1.0.1.tgz" => {
'data' => 'aGVsbG8K',
'length' => 8
}
}
}
end
end
def expect_a_valid_package_response
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/json')
expect(response).to match_response_schema('public_api/v4/packages/npm_package', dir: 'ee')
expect(json_response['name']).to eq(package.name)
expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version', dir: 'ee')
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::CreateNpmPackageService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:version) { '1.0.1'.freeze }
let(:params) do
{
name: package_name,
versions: {
version => {
dist: {
shasum: 'f572d396fae9206628714fb2ce00f72e94f2258f'
}
}
},
'_attachments' => {
"#{package_name}-#{version}.tgz" => {
'content_type' => 'application/octet-stream',
'data' => 'aGVsbG8K',
'length' => 8
}
}
}
end
shared_examples 'valid package' do
it 'creates a valid package' do
package = described_class.new(project, user, params).execute
expect(package).to be_valid
expect(package.name).to eq(package_name)
expect(package.version).to eq(version)
end
end
describe '#execute' do
context 'scoped package' do
let(:package_name) { '@gitlab/my-app'.freeze }
it_behaves_like 'valid package'
end
context 'normal package' do
let(:package_name) { 'my-app'.freeze }
it_behaves_like 'valid package'
end
end
end
# frozen_string_literal: true
module EE
module StubObjectStorage
def stub_packages_object_storage(**params)
stub_object_storage_uploader(config: ::Gitlab.config.packages.object_store,
uploader: ::Packages::PackageFileUploader,
remote_directory: 'packages',
**params)
end
end
end
...@@ -34,11 +34,4 @@ describe 'gitlab:packages namespace rake task' do ...@@ -34,11 +34,4 @@ describe 'gitlab:packages namespace rake task' do
end end
end end
end end
def stub_packages_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.packages.object_store,
uploader: ::Packages::PackageFileUploader,
remote_directory: 'packages',
**params)
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::PackageFileUploader do
let(:package_file) { create(:package_file, :xml) }
let(:uploader) { described_class.new(package_file, :file) }
let(:path) { Gitlab.config.packages.storage_path }
subject { uploader }
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}],
cache_dir: %r[/packages/tmp/cache],
work_dir: %r[/packages/tmp/work]
context 'object store is remote' do
before do
stub_packages_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
describe 'remote file' do
let(:package_file) { create(:package_file, :object_storage, :xml) }
context 'with object storage enabled' do
before do
stub_packages_object_storage
end
it 'can store file remotely' do
allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
package_file
expect(package_file.file_store).to eq(described_class::Store::REMOTE)
expect(package_file.file.path).not_to be_blank
end
end
end
end
...@@ -5363,9 +5363,6 @@ msgstr "" ...@@ -5363,9 +5363,6 @@ msgstr ""
msgid "Maven Metadata" msgid "Maven Metadata"
msgstr "" msgstr ""
msgid "Maven package"
msgstr ""
msgid "Max access level" msgid "Max access level"
msgstr "" msgstr ""
......
module StubObjectStorage module StubObjectStorage
prepend EE::StubObjectStorage
def stub_object_storage_uploader( def stub_object_storage_uploader(
config:, config:,
uploader:, uploader:,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment