diff --git a/app/services/packages/nuget/odata_package_entry_service.rb b/app/services/packages/nuget/odata_package_entry_service.rb index 0cdcc38de16965c2dc10d268b0aa6a0b11b26dce..679b01d6c4865fcb3da52391340ac2ae173fb977 100644 --- a/app/services/packages/nuget/odata_package_entry_service.rb +++ b/app/services/packages/nuget/odata_package_entry_service.rb @@ -5,9 +5,6 @@ module Nuget class OdataPackageEntryService include API::Helpers::RelatedResourcesHelpers - SEMVER_LATEST_VERSION_PLACEHOLDER = '0.0.0-latest-version' - LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT = 'latest' - def initialize(project, params) @project = project @params = params @@ -29,42 +26,40 @@ def package_entry #{params[:package_name]} - #{package_version} + #{params[:package_version]} XML end - def package_version - params[:package_version] || SEMVER_LATEST_VERSION_PLACEHOLDER - end - def id_url expose_url "#{api_v4_projects_packages_nuget_v2_path(id: project.id)}" \ - "/Packages(Id='#{params[:package_name]}',Version='#{package_version}')" + "/Packages(Id='#{params[:package_name]}',Version='#{params[:package_version]}')" end - # TODO: use path helper when download endpoint is merged def download_url - expose_url "#{api_v4_projects_packages_nuget_v2_path(id: project.id)}" \ - "/download/#{params[:package_name]}/#{download_url_package_version}" - end - - def download_url_package_version - if latest_version? - LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT + if params[:package_version].present? + expose_url api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path( + { + id: project.id, + package_name: params[:package_name], + package_version: params[:package_version], + package_filename: file_name + }, + true + ) else - params[:package_version] + xml_base end end - def latest_version? - params[:package_version].nil? || params[:package_version] == SEMVER_LATEST_VERSION_PLACEHOLDER - end - def xml_base expose_url api_v4_projects_packages_nuget_v2_path(id: project.id) end + + def file_name + "#{params[:package_name]}.#{params[:package_version]}.nupkg" + end end end end diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md index ee304ab28df3f394f3dde9d81e9a3aa29ed820da..e28f42fa931516d27b0f60556a355697890b9541 100644 --- a/doc/api/packages/nuget.md +++ b/doc/api/packages/nuget.md @@ -488,10 +488,29 @@ Example response: | `GET projects/:id/packages/nuget/v2/FindPackagesById()?id=''` | Returns an OData XML document containing information about the package with the given name. | | `GET projects/:id/packages/nuget/v2/Packages(Id='',Version='')` | Returns an OData XML document containing information about the package with the given name and version. | +```shell +curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2/Packages(Id='mynugetpkg',Version='1.0.0')" +``` + +Example response: + +```xml + + https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2/Packages(Id='mynugetpkg',Version='1.0.0') + + mynugetpkg + + + 1.0.0 + + +``` + NOTE: -GitLab doesn't receive an authentication token for the `Packages()` and `FindPackagesByID()` endpoints. -To not reveal the package version to unauthenticated users, the actual latest package version is not returned. Instead, a placeholder version is returned. -The latest version is obtained in the subsequent download request where the authentication token is sent. +GitLab doesn't receive an authentication token for the `Packages()` and +`FindPackagesByID()` endpoints, so the latest version of the package +cannot be returned. You must provide the version when you install +or upgrade a package with the NuGet v2 feed. ```shell curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2/Packages()?$filter=(tolower(Id) eq 'mynugetpkg')" @@ -501,12 +520,12 @@ Example response: ```xml - https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2/Packages(Id='mynugetpkg',Version='0.0.0-latest-version') + https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2/Packages(Id='mynugetpkg',Version='') mynugetpkg - + - 0.0.0-latest-version + ``` diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md index 340df4a3c5fb3d5fb9e7b3e7d6dd18cf9b019f84..4f8bb56bda8521aadc203b4e2cd9b162606d1e3b 100644 --- a/doc/user/packages/nuget_repository/index.md +++ b/doc/user/packages/nuget_repository/index.md @@ -492,6 +492,55 @@ dotnet add package \ - `` is the package ID. - `` is the package version. Optional. +### Install a package using NuGet v2 feed + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416405) in GitLab 16.5. + +Prerequisites: + +- The project-level Package Registry is a [v2 feed source](#add-a-source-with-chocolatey-cli) for Chocolatey. +- A version must be provided when installing or upgrading a package using NuGet v2 feed. + +To install a package with the Chocolatey CLI: + +```shell +choco install -Source -Version +``` + +In this command: + +- `` is the package ID. +- `` is the URL or name of the NuGet v2 feed Package Registry. +- `` is the package version. + +For example: + +```shell +choco install MyPackage -Source gitlab -Version 1.0.2 + +# or + +choco install MyPackage -Source "https://gitlab.example.com/api/v4/projects//packages/nuget/v2" -u -p -Version 1.0.2 +``` + +To upgrade a package with the Chocolatey CLI: + +```shell +choco upgrade -Source -Version +``` + +In this command: + +- `` is the package ID. +- `` is the URL or name of the NuGet v2 feed Package Registry. +- `` is the package version. + +For example: + +```shell +choco upgrade MyPackage -Source gitlab -Version 1.0.3 +``` + ## Symbol packages > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/262081) in GitLab 14.1. diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb index dbc789c68b6c7b6983557875dfe1ba34cfaa6898..3da05b4e7d928abbeb26cd1b3d035281e01e2c2c 100644 --- a/lib/api/nuget_project_packages.rb +++ b/lib/api/nuget_project_packages.rb @@ -113,9 +113,7 @@ def publish_package(symbol_package: false) 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') } + **track_package_event_attrs(package.project) ) end rescue ObjectStorage::RemoteStoreError => e @@ -148,6 +146,16 @@ def present_odata_entry present odata_entry end + + def track_package_event_attrs(project) + attrs = { + category: 'API::NugetPackages', + project: project, + namespace: project.namespace + } + attrs[:feed] = 'v2' if request.path.include?('nuget/v2') + attrs + end end params do @@ -216,9 +224,7 @@ def present_odata_entry track_package_event( params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package', :nuget, - category: 'API::NugetPackages', - project: package_file.project, - namespace: package_file.project.namespace + **track_package_event_attrs(package.project) ) # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb index b55d992c1e44c508f27ba61f3b43b120f53c81fe..1dcae4cf29503776b16cbaf9f499c756f86f237d 100644 --- a/spec/requests/api/nuget_project_packages_spec.rb +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -203,11 +203,12 @@ def snowplow_context(user_role: :developer) describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do let_it_be(:package) { create(:nuget_package, :with_symbol_package, :with_metadatum, project: project, name: package_name, version: '0.1') } + let_it_be(:package_version) { package.version } let(:format) { 'nupkg' } - let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.#{format}" } + let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{package_version}/#{package.name}.#{package_version}.#{format}" } - subject { get api(url) } + subject { get api(url), headers: headers } context 'with valid target' do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do @@ -236,8 +237,6 @@ def snowplow_context(user_role: :developer) let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } - subject { get api(url), headers: headers } - before do update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end diff --git a/spec/services/packages/nuget/odata_package_entry_service_spec.rb b/spec/services/packages/nuget/odata_package_entry_service_spec.rb index d4c47538ce24b894511064ae2f4bbec71b7f3569..b4a22fef32b99cdb691c1d12db31b1465fc42378 100644 --- a/spec/services/packages/nuget/odata_package_entry_service_spec.rb +++ b/spec/services/packages/nuget/odata_package_entry_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Packages::Nuget::OdataPackageEntryService, feature_category: :package_registry do + include GrapePathHelpers::NamedRouteMatcher + let_it_be(:project) { build_stubbed(:project) } let_it_be(:params) { { package_name: 'dummy', package_version: '1.0.0' } } let(:doc) { Nokogiri::XML(subject.payload) } @@ -10,7 +12,7 @@ subject { described_class.new(project, params).execute } describe '#execute' do - shared_examples 'returning a package entry with the correct attributes' do |pkg_version, content_url_pkg_version| + shared_examples 'returning a package entry with the correct attributes' do |pkg_version = ''| it 'returns a package entry with the correct attributes' do expect(doc.root.name).to eq('entry') expect(doc_node('id').text).to include( @@ -18,7 +20,7 @@ ) expect(doc_node('title').text).to eq(params[:package_name]) expect(doc_node('content').attr('src')).to include( - content_url(project.id, params[:package_name], content_url_pkg_version) + content_url(project.id, params[:package_name], pkg_version) ) expect(doc_node('Version').text).to eq(pkg_version) end @@ -29,29 +31,17 @@ expect(subject).to be_success end - it_behaves_like 'returning a package entry with the correct attributes', '1.0.0', '1.0.0' + it_behaves_like 'returning a package entry with the correct attributes', '1.0.0' end - context 'when package_version is nil' do + context 'when package_version is not present' do let(:params) { { package_name: 'dummy', package_version: nil } } it 'returns a success ServiceResponse' do expect(subject).to be_success end - it_behaves_like 'returning a package entry with the correct attributes', - described_class::SEMVER_LATEST_VERSION_PLACEHOLDER, described_class::LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT - end - - context 'when package_version is 0.0.0-latest-version' do - let(:params) { { package_name: 'dummy', package_version: described_class::SEMVER_LATEST_VERSION_PLACEHOLDER } } - - it 'returns a success ServiceResponse' do - expect(subject).to be_success - end - - it_behaves_like 'returning a package entry with the correct attributes', - described_class::SEMVER_LATEST_VERSION_PLACEHOLDER, described_class::LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT + it_behaves_like 'returning a package entry with the correct attributes' end end @@ -64,6 +54,13 @@ def id_url(id, package_name, package_version) end def content_url(id, package_name, package_version) - "api/v4/projects/#{id}/packages/nuget/v2/download/#{package_name}/#{package_version}" + if package_version.present? + filename = "#{package_name}.#{package_version}.nupkg" + api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path( + { id: id, package_name: package_name, package_version: package_version, package_filename: filename }, true + ) + else + api_v4_projects_packages_nuget_v2_path(id: id) + end 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 1be99040ae5de76c14039855f9ae77d993c85bed..0b67f12fb8dc0dd988195850f772dc0987531a4b 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 @@ -357,12 +357,7 @@ end context 'with normalized package version' do - let(:normalized_version) { '0.1.0' } - let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{normalized_version}/#{package.name}.#{package.version}.#{format}" } - - before do - package.nuget_metadatum.update_column(:normalized_version, normalized_version) - end + let(:package_version) { '0.1.0' } it_behaves_like 'returning response status', status