diff --git a/app/presenters/packages/nuget/v2/service_index_presenter.rb b/app/presenters/packages/nuget/v2/service_index_presenter.rb new file mode 100644 index 0000000000000000000000000000000000000000..a8fc9b673bfa78b3a7ca45db420b11cfddb5f920 --- /dev/null +++ b/app/presenters/packages/nuget/v2/service_index_presenter.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Packages + module Nuget + module V2 + class ServiceIndexPresenter + include API::Helpers::RelatedResourcesHelpers + + ROOT_ATTRIBUTES = { + xmlns: 'http://www.w3.org/2007/app', + 'xmlns:atom' => 'http://www.w3.org/2005/Atom' + }.freeze + + def initialize(project_or_group) + @project_or_group = project_or_group + end + + def xml + Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.service(ROOT_ATTRIBUTES.merge('xml:base' => xml_base)) do + xml.workspace do + xml['atom'].title('Default', type: 'text') + xml.collection(href: 'Packages') do + xml['atom'].title('Packages', type: 'text') + end + end + end + end + end + + private + + attr_reader :project_or_group + + def xml_base + base_path = case project_or_group + when Project + api_v4_projects_packages_nuget_v2_path(id: project_or_group.id) + when Group + api_v4_groups___packages_nuget_v2_path(id: project_or_group.id) + end + + expose_url(base_path) + end + end + end + end +end diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md index afe384b5a29c6d82b72f378cc29426713d63ea22..cbfd12c72339b4231d071a025b05056d4335f856 100644 --- a/doc/api/packages/nuget.md +++ b/doc/api/packages/nuget.md @@ -80,13 +80,22 @@ This writes the downloaded file to `MyNuGetPkg.1.3.0.17.nupkg` in the current di ## Upload a package file -> Introduced in GitLab 12.8. +> - Introduced in GitLab 12.8 for NuGet v3 feed. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2 for NuGet v2 feed. Upload a NuGet package file: -```plaintext -PUT projects/:id/packages/nuget -``` +- For NuGet v3 feed: + + ```plaintext + PUT projects/:id/packages/nuget + ``` + +- For NuGet V2 feed: + + ```plaintext + PUT projects/:id/packages/nuget/v2 + ``` | Attribute | Type | Required | Description | | ----------------- | ------ | -------- | ----------- | @@ -95,12 +104,23 @@ PUT projects/:id/packages/nuget | `package_version` | string | yes | The version of the package. | | `package_filename`| string | yes | The name of the file. | -```shell -curl --request PUT \ - --form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \ - --user <username>:<personal_access_token> \ - "https://gitlab.example.com/api/v4/projects/1/packages/nuget/" -``` +- For NuGet v3 feed: + + ```shell + curl --request PUT \ + --form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \ + --user <username>:<personal_access_token> \ + "https://gitlab.example.com/api/v4/projects/1/packages/nuget/" + ``` + +- For NuGet v2 feed: + + ```shell + curl --request PUT \ + --form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \ + --user <username>:<personal_access_token> \ + "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2" + ``` ## Upload a symbol package file @@ -158,6 +178,37 @@ The examples in this document all use the project-level prefix. ## Service Index +### V2 source feed/protocol + +Returns an XML document that represents the service index of the v2 NuGet source feed. +Authentication is not required: + +```plaintext +GET <route-prefix>/v2 +``` + +Example Request: + +```shell +curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2" +``` + +Example response: + +```xml +<?xml version="1.0" encoding="utf-8"?> +<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"> + <workspace> + <atom:title type="text">Default</atom:title> + <collection href="Packages"> + <atom:title type="text">Packages</atom:title> + </collection> + </workspace> +</service> +``` + +### V3 source feed/protocol + > - Introduced in GitLab 12.6. > - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214674) to be public in GitLab 16.1. diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md index ea9bfd7defb93491a47c249900a0d60029d22088..fb457bb98b51c715b7dac335bdd78f4fe049da68 100644 --- a/doc/user/packages/nuget_repository/index.md +++ b/doc/user/packages/nuget_repository/index.md @@ -70,6 +70,7 @@ You can now add a new source to NuGet with: - [Visual Studio](#add-a-source-with-visual-studio) - [.NET CLI](#add-a-source-with-the-net-cli) - [Configuration file](#add-a-source-with-a-configuration-file) +- [Chocolatey CLI](#add-a-source-with-chocolatey-cli) ### Add a source with the NuGet CLI @@ -281,6 +282,22 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Re export GITLAB_PACKAGE_REGISTRY_PASSWORD=<gitlab_personal_access_token or deploy_token> ``` +### Add a source with Chocolatey CLI + +You can add a source feed with the Chocolatey CLI. If you use Chocolatey CLI v1.x, you can add only a NuGet v2 source feed. + +#### Configure a project-level endpoint + +You need a project-level endpoint to publish NuGet packages to the Package Registry. + +To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Registry as a source for Chocolatey: + +- Add the Package Registry as a source with `choco`: + + ```shell + choco source add -n=gitlab -s "'https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2'" -u=<gitlab_username or deploy_token_username> -p=<gitlab_personal_access_token or deploy_token> + ``` + ## Publish a NuGet package Prerequisite: @@ -385,6 +402,31 @@ updated: 1. Commit the changes and push it to your GitLab repository to trigger a new CI/CD build. +### Publish a NuGet package with Chocolatey CLI + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2. + +Prerequisite: + +- The project-level Package Registry is a source for Chocolatey. + +To publish a package with the Chocolatey CLI: + +```shell +choco push <package_file> --source <source_url> --api-key <gitlab_personal_access_token, deploy_token or job token> +``` + +In this command: + +- `<package_file>` is your package filename and ends with `.nupkg`. +- `<source_url>` is the URL of the NuGet v2 feed Package Registry. + +For example: + +```shell +choco push MyPackage.1.0.0.nupkg --source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2" --api-key <gitlab_personal_access_token, deploy_token or job token> +``` + ### Publishing a package with the same name or version When you publish a package with the same name or version as an existing package, diff --git a/lib/api/concerns/packages/nuget/public_endpoints.rb b/lib/api/concerns/packages/nuget/public_endpoints.rb index 37b503212d92aae6f21bf839f64b99f219101036..d5be136c7a28d8795a1af38bd154206467f3d9f2 100644 --- a/lib/api/concerns/packages/nuget/public_endpoints.rb +++ b/lib/api/concerns/packages/nuget/public_endpoints.rb @@ -16,7 +16,7 @@ module PublicEndpoints included do # https://docs.microsoft.com/en-us/nuget/api/service-index - desc 'The NuGet Service Index' do + desc 'The NuGet V3 Feed Service Index' do detail 'This feature was introduced in GitLab 12.6' success code: 200, model: ::API::Entities::Nuget::ServiceIndex failure [ @@ -34,6 +34,33 @@ module PublicEndpoints present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group_without_auth), with: ::API::Entities::Nuget::ServiceIndex end + + desc 'The NuGet V2 Feed Service Index' do + detail 'This feature was introduced in GitLab 16.2' + success code: 200 + failure [ + { code: 404, message: 'Not Found' } + ] + tags %w[nuget_packages] + end + namespace '/v2' do + get format: :xml, urgency: :low do + env['api.format'] = :xml + content_type 'application/xml; charset=utf-8' + # needed to allow browser default inline styles in xml response + header 'Content-Security-Policy', "nonce-#{SecureRandom.base64(16)}" + + track_package_event( + 'cli_metadata', + :nuget, + **snowplow_gitlab_standard_context_without_auth.merge(category: 'API::NugetPackages', feed: 'v2') + ) + + present ::Packages::Nuget::V2::ServiceIndexPresenter + .new(project_or_group_without_auth) + .xml + end + end end end end diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb index 2716d6f0b64a877c13d36b1ca94f668fd7802b48..1631f7b2a9be72425d45681d53f307f936a49ff0 100644 --- a/lib/api/nuget_project_packages.rb +++ b/lib/api/nuget_project_packages.rb @@ -98,6 +98,22 @@ def upload_nuget_package_file(symbol_package: false) created! end + def publish_package(symbol_package: false) + upload_nuget_package_file(symbol_package: symbol_package) do |package| + track_package_event( + symbol_package ? 'push_symbol_package' : 'push_package', + :nuget, + **{ category: 'API::NugetPackages', + project: package.project, + namespace: package.project.namespace }.tap { |args| args[:feed] = 'v2' if request.path.include?('nuget/v2') } + ) + end + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id }) + + forbidden! + end + def required_permission :read_package end @@ -179,7 +195,7 @@ def required_permission end end - # To support an additional authentication option for download endpoints, + # To support an additional authentication option for publish endpoints, # we redefine the `authenticate_with` method by combining the previous # authentication option with the new one. authenticate_with do |accept| @@ -191,7 +207,7 @@ def required_permission namespace '/nuget' do # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource - desc 'The NuGet Package Publish endpoint' do + desc 'The NuGet V3 Feed Package Publish endpoint' do detail 'This feature was introduced in GitLab 12.6' success code: 201 failure [ @@ -207,19 +223,7 @@ def required_permission use :file_params end put urgency: :low do - upload_nuget_package_file do |package| - track_package_event( - 'push_package', - :nuget, - category: 'API::NugetPackages', - project: package.project, - namespace: package.project.namespace - ) - end - rescue ObjectStorage::RemoteStoreError => e - Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id }) - - forbidden! + publish_package end desc 'The NuGet Package Authorize endpoint' do @@ -252,19 +256,7 @@ def required_permission use :file_params end put 'symbolpackage', urgency: :low do - upload_nuget_package_file(symbol_package: true) do |package| - track_package_event( - 'push_symbol_package', - :nuget, - category: 'API::NugetPackages', - project: package.project, - namespace: package.project.namespace - ) - end - rescue ObjectStorage::RemoteStoreError => e - Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id }) - - forbidden! + publish_package(symbol_package: true) end desc 'The NuGet Symbol Package Authorize endpoint' do @@ -280,6 +272,42 @@ def required_permission put 'symbolpackage/authorize', urgency: :low do authorize_nuget_upload end + + namespace '/v2' do + desc 'The NuGet V2 Feed Package Publish endpoint' do + detail 'This feature was introduced in GitLab 16.2' + success code: 201 + failure [ + { code: 400, message: 'Bad Request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[nuget_packages] + end + + params do + use :file_params + end + put do + publish_package + end + + desc 'The NuGet V2 Feed Package Authorize endpoint' do + detail 'This feature was introduced in GitLab 16.2' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[nuget_packages] + end + + put 'authorize', urgency: :low do + authorize_nuget_upload + end + end end end end diff --git a/spec/presenters/packages/nuget/v2/service_index_presenter_spec.rb b/spec/presenters/packages/nuget/v2/service_index_presenter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..09dd3ff7fe4911080674fc8d770c2cc30f0a3a32 --- /dev/null +++ b/spec/presenters/packages/nuget/v2/service_index_presenter_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Nuget::V2::ServiceIndexPresenter, feature_category: :package_registry do + let_it_be(:project) { build_stubbed(:project) } + let_it_be(:group) { build_stubbed(:group) } + + describe '#xml' do + let(:project_or_group) { project } + let(:presenter) { described_class.new(project_or_group) } + let(:xml_doc) { Nokogiri::XML::Document.parse(presenter.xml.to_xml) } + let(:service_node) { xml_doc.at_xpath('//xmlns:service') } + + it { expect(xml_doc.root.name).to eq('service') } + + it 'includes the workspace and collection nodes' do + workspace = xml_doc.at_xpath('//xmlns:service/xmlns:workspace') + collection = xml_doc.at_xpath('//xmlns:service/xmlns:workspace/xmlns:collection') + + expect(workspace).to be_present + expect(workspace.children).to include(collection) + expect(collection).to be_present + end + + it 'sets the appropriate XML namespaces on the root node' do + expect(service_node.namespaces['xmlns']).to eq('http://www.w3.org/2007/app') + expect(service_node.namespaces['xmlns:atom']).to eq('http://www.w3.org/2005/Atom') + end + + context 'when the presenter is initialized with a project' do + it 'sets the XML base path correctly for a project scope' do + expect(service_node['xml:base']).to include(expected_xml_base(project)) + end + end + + context 'when the presenter is initialized with a group' do + let(:project_or_group) { group } + + it 'sets the XML base path correctly for a group scope' do + expect(service_node['xml:base']).to include(expected_xml_base(group)) + end + end + end + + def expected_xml_base(project_or_group) + case project_or_group + when Project + api_v4_projects_packages_nuget_v2_path(id: project_or_group.id) + when Group + api_v4_groups___packages_nuget_v2_path(id: project_or_group.id) + end + end +end diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index 07199119cb583610caf6af4aba5e03c393665425..92eb869b871023415a67211fda738dc40690b447 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -31,6 +31,12 @@ def snowplow_context(user_role: :developer) end end + describe 'GET /api/v4/groups/:id/-/packages/nuget/v2' do + it_behaves_like 'handling nuget service requests', v2: true do + let(:url) { "/groups/#{target.id}/-/packages/nuget/v2" } + end + end + describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do it_behaves_like 'handling nuget metadata requests with package name', example_names_with_status: diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb index 887dfd4beeb83e6f3df69a3df9e356c819d72906..40068f101f788896ce25f105d6d88316b19ed94b 100644 --- a/spec/requests/api/nuget_project_packages_spec.rb +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -42,6 +42,14 @@ def snowplow_context(user_role: :developer) it_behaves_like 'accept get request on private project with access to package registry for everyone' end + describe 'GET /api/v4/projects/:id/packages/nuget/v2' do + let(:url) { "/projects/#{target.id}/packages/nuget/v2" } + + it_behaves_like 'handling nuget service requests', v2: true + + it_behaves_like 'accept get request on private project with access to package registry for everyone' + end + describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" } @@ -183,75 +191,39 @@ def snowplow_context(user_role: :developer) end describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do - include_context 'workhorse headers' - - let(:url) { "/projects/#{target.id}/packages/nuget/authorize" } - let(:headers) { {} } - - subject { put api(url), headers: headers } - - it_behaves_like 'nuget authorize upload endpoint' + it_behaves_like 'nuget authorize upload endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget/authorize" } + end end describe 'PUT /api/v4/projects/:id/packages/nuget' do - include_context 'workhorse headers' - - let_it_be(:file_name) { 'package.nupkg' } - - let(:url) { "/projects/#{target.id}/packages/nuget" } - let(:headers) { {} } - let(:params) { { package: temp_file(file_name) } } - let(:file_key) { :package } - let(:send_rewritten_field) { true } - - subject do - workhorse_finalize( - api(url), - method: :put, - file_key: file_key, - params: params, - headers: headers, - send_rewritten_field: send_rewritten_field - ) + it_behaves_like 'nuget upload endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget" } end - - it_behaves_like 'nuget upload endpoint' end describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage/authorize' do - include_context 'workhorse headers' - - let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" } - let(:headers) { {} } - - subject { put api(url), headers: headers } - - it_behaves_like 'nuget authorize upload endpoint' + it_behaves_like 'nuget authorize upload endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" } + end end describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage' do - include_context 'workhorse headers' - - let_it_be(:file_name) { 'package.snupkg' } - - let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" } - let(:headers) { {} } - let(:params) { { package: temp_file(file_name) } } - let(:file_key) { :package } - let(:send_rewritten_field) { true } - - subject do - workhorse_finalize( - api(url), - method: :put, - file_key: file_key, - params: params, - headers: headers, - send_rewritten_field: send_rewritten_field - ) + it_behaves_like 'nuget upload endpoint', symbol_package: true do + let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" } end + end - it_behaves_like 'nuget upload endpoint', symbol_package: true + describe 'PUT /api/v4/projects/:id/packages/nuget/v2/authorize' do + it_behaves_like 'nuget authorize upload endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget/v2/authorize" } + end + end + + describe 'PUT /api/v4/projects/:id/packages/nuget/v2' do + it_behaves_like 'nuget upload endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget/v2" } + end end def update_visibility_to(visibility) diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index 432e67ee21e75a21a41fd51f653a3bca63d5fead..150e9a4e00436ede81466ece2632f6ba97e735c2 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -RSpec.shared_examples 'handling nuget service requests' do +RSpec.shared_examples 'handling nuget service requests' do |v2: false| subject { get api(url) } + it { is_expected.to have_request_urgency(v2 ? :low : :default) } + context 'with valid target' do using RSpec::Parameterized::TableSyntax @@ -20,15 +22,17 @@ end with_them do - let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) } - - subject { get api(url) } + let(:snowplow_gitlab_standard_context) do + snowplow_context(user_role: :anonymous).tap do |ctx| + ctx[:feed] = 'v2' if v2 + end + end before do update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], v2 end end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index d6a0055700dbf6152ead09711759ae57b5f1ae1e..0f1ec14a0b6ec4eab10e16d70b8940b95423c262 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -18,7 +18,7 @@ end end -RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true| +RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true, v2 = false| context "for user type #{user_type}" do before do target.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -28,15 +28,22 @@ it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata' - it 'returns a valid json response' do + it 'returns a valid json or xml response' do subject - expect(response.media_type).to eq('application/json') - expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index') - expect(json_response).to be_a(Hash) + if v2 + expect(response.media_type).to eq('application/xml') + expect(body).to have_xpath('//service') + .and have_xpath('//service/workspace') + .and have_xpath('//service/workspace/collection[@href]') + else + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index') + expect(json_response).to be_a(Hash) + end end - context 'with invalid format' do + context 'with invalid format', unless: v2 do let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found @@ -439,6 +446,13 @@ RSpec.shared_examples 'nuget authorize upload endpoint' do using RSpec::Parameterized::TableSyntax + include_context 'workhorse headers' + + let(:headers) { {} } + + subject { put api(url), headers: headers } + + it { is_expected.to have_request_urgency(:low) } context 'with valid project' do where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do @@ -517,6 +531,26 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| using RSpec::Parameterized::TableSyntax + include_context 'workhorse headers' + + let(:headers) { {} } + let(:file_name) { symbol_package ? 'package.snupkg' : 'package.nupkg' } + let(:params) { { package: temp_file(file_name) } } + let(:file_key) { :package } + let(:send_rewritten_field) { true } + + subject do + workhorse_finalize( + api(url), + method: :put, + file_key: file_key, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + end + + it { is_expected.to have_request_urgency(:low) } context 'with valid project' do where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do @@ -573,7 +607,12 @@ end let(:headers) { user_headers.merge(workhorse_headers) } - let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } } + + let(:snowplow_gitlab_standard_context) do + { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' }.tap do |ctx| + ctx[:feed] = 'v2' if url.include?('nuget/v2') + end + end before do update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) @@ -604,4 +643,16 @@ it_behaves_like 'returning response status', :bad_request end + + context 'when ObjectStorage::RemoteStoreError is raised' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + + before do + allow_next_instance_of(::Packages::CreatePackageFileService) do |instance| + allow(instance).to receive(:execute).and_raise(ObjectStorage::RemoteStoreError) + end + end + + it_behaves_like 'returning response status', :forbidden + end end diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index dd1725a723e515310e4a1127cc8b9bee0999e1a4..e634f0ca66cccfdbedd062c04418e81c9861c7cd 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -285,6 +285,9 @@ func configureRoutes(u *upstream) { // NuGet Artifact Repository u.route("PUT", apiProjectPattern+`/packages/nuget/`, mimeMultipartUploader), + // NuGet v2 Artifact Repository + u.route("PUT", apiProjectPattern+`/packages/nuget/v2`, mimeMultipartUploader), + // PyPI Artifact Repository u.route("POST", apiProjectPattern+`/packages/pypi`, mimeMultipartUploader), diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go index 8effe29197930a712b70de2627a325dc93d69af6..62a78dd9464b48b8bf177d9afe1a62340e1918ad 100644 --- a/workhorse/upload_test.go +++ b/workhorse/upload_test.go @@ -159,6 +159,9 @@ func TestAcceleratedUpload(t *testing.T) { {"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true}, {"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true}, {"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true}, + {"PUT", "/api/v4/projects/9001/packages/nuget/v2/files", true}, + {"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v2/files", true}, + {"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v2/files", true}, {"POST", `/api/v4/groups/import`, true}, {"POST", `/api/v4/groups/import/`, true}, {"POST", `/api/v4/projects/import`, true}, @@ -289,6 +292,8 @@ func TestUnacceleratedUploads(t *testing.T) { {"POST", `/api/v4/projects/group/project/wikis/attachments`}, {"PUT", "/api/v4/projects/group/subgroup/project/packages/nuget/v1/files"}, {"PUT", "/api/v4/projects/group/project/packages/nuget/v1/files"}, + {"POST", "/api/v4/projects/group/subgroup/project/packages/nuget/v2/files"}, + {"POST", "/api/v4/projects/group/project/packages/nuget/v2/files"}, {"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`}, {"POST", `/api/v4/projects/group/project/packages/pypi`}, {"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},