Commit f8858c8f authored by Steve Abrams's avatar Steve Abrams

RubyGems create package service and upload routes

Adds the create service to create the temporary
package record for an uploaded rubygems package.

Implements the two rubygems upload routes.
parent 0bf9d165
# frozen_string_literal: true # frozen_string_literal: true
module Packages module Packages
module Nuget module Nuget
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
def self.table_name_prefix def self.table_name_prefix
'packages_nuget_' 'packages_nuget_'
end end
......
...@@ -98,12 +98,12 @@ class Packages::Package < ApplicationRecord ...@@ -98,12 +98,12 @@ class Packages::Package < ApplicationRecord
end end
scope :preload_composer, -> { preload(:composer_metadatum) } scope :preload_composer, -> { preload(:composer_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) } scope :has_version, -> { where.not(version: nil) }
scope :processed, -> do scope :processed, -> do
where.not(package_type: :nuget).or( where.not(package_type: :nuget).or(
where.not(name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME)
) )
end end
scope :preload_files, -> { preload(:package_files) } scope :preload_files, -> { preload(:package_files) }
......
# frozen_string_literal: true
module Packages
module Rubygems
TEMPORARY_PACKAGE_NAME = 'Gem.Temporary.Package'
def self.table_name_prefix
'packages_rubygems_'
end
end
end
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
module Packages module Packages
module Rubygems module Rubygems
class Metadatum < ApplicationRecord class Metadatum < ApplicationRecord
self.table_name = 'packages_rubygems_metadata'
self.primary_key = :package_id self.primary_key = :package_id
belongs_to :package, -> { where(package_type: :rubygems) }, inverse_of: :rubygems_metadatum belongs_to :package, -> { where(package_type: :rubygems) }, inverse_of: :rubygems_metadatum
......
# frozen_string_literal: true
module Packages
class CreateTemporaryPackageService < ::Packages::CreatePackageService
PACKAGE_VERSION = '0.0.0'
def execute(package_type, name: 'Temporary.Package')
create_package!(package_type,
name: name,
version: "#{PACKAGE_VERSION}-#{uuid}",
status: 'processing'
)
end
private
def uuid
SecureRandom.uuid
end
end
end
# frozen_string_literal: true
module Packages
module Nuget
class CreatePackageService < ::Packages::CreatePackageService
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
PACKAGE_VERSION = '0.0.0'
def execute
create_package!(:nuget,
name: TEMPORARY_PACKAGE_NAME,
version: "#{PACKAGE_VERSION}-#{uuid}"
)
end
private
def uuid
SecureRandom.uuid
end
end
end
end
...@@ -68,7 +68,8 @@ module Packages ...@@ -68,7 +68,8 @@ module Packages
def update_linked_package def update_linked_package
@package_file.package.update!( @package_file.package.update!(
name: package_name, name: package_name,
version: package_version version: package_version,
status: :default
) )
::Packages::Nuget::CreateDependencyService.new(@package_file.package, package_dependencies) ::Packages::Nuget::CreateDependencyService.new(@package_file.package, package_dependencies)
......
...@@ -103,8 +103,10 @@ class Gitlab::Seeder::Packages ...@@ -103,8 +103,10 @@ class Gitlab::Seeder::Packages
name = "MyNugetApp.Package#{i}" name = "MyNugetApp.Package#{i}"
version = "4.2.#{i}" version = "4.2.#{i}"
pkg = ::Packages::Nuget::CreatePackageService.new(project, project.creator, {}).execute pkg = ::Packages::CreateTemporaryPackageService.new(
# when using ::Packages::Nuget::CreatePackageService, packages have a fixed name and a fixed version. project, project.creator, {}
).execute(:nuget, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME)
# when using ::Packages::CreateTemporaryPackageService, packages have a fixed name and a fixed version.
pkg.update!(name: name, version: version) pkg.update!(name: name, version: version)
filename = 'package.nupkg' filename = 'package.nupkg'
......
...@@ -62,8 +62,9 @@ module API ...@@ -62,8 +62,9 @@ module API
file_name: PACKAGE_FILENAME file_name: PACKAGE_FILENAME
) )
package = ::Packages::Nuget::CreatePackageService.new(project_or_group, current_user, declared_params.merge(build: current_authenticated_job)) package = ::Packages::CreateTemporaryPackageService.new(
.execute project_or_group, current_user, declared_params.merge(build: current_authenticated_job)
).execute(:nuget, name: ::Packages::Nuget::TEMPORARY_PACKAGE_NAME)
package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)) package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job))
.execute .execute
......
...@@ -12,7 +12,7 @@ module API ...@@ -12,7 +12,7 @@ module API
# The Marshal version can be found by "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" # The Marshal version can be found by "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
# Updating the version should require a GitLab API version change. # Updating the version should require a GitLab API version change.
MARSHAL_VERSION = '4.8' MARSHAL_VERSION = '4.8'
PACKAGE_FILENAME = 'package.gem'
FILE_NAME_REQUIREMENTS = { FILE_NAME_REQUIREMENTS = {
file_name: API::NO_SLASH_URL_PART_REGEX file_name: API::NO_SLASH_URL_PART_REGEX
}.freeze }.freeze
...@@ -73,16 +73,45 @@ module API ...@@ -73,16 +73,45 @@ module API
detail 'This feature was introduced in GitLab 13.9' detail 'This feature was introduced in GitLab 13.9'
end end
post 'gems/authorize' do post 'gems/authorize' do
# To be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/299263 authorize_workhorse!(
not_found! subject: user_project,
has_length: false,
maximum_size: user_project.actual_limits.rubygems_max_file_size
)
end end
desc 'Upload a gem' do desc 'Upload a gem' do
detail 'This feature was introduced in GitLab 13.9' detail 'This feature was introduced in GitLab 13.9'
end end
params do
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
end
post 'gems' do post 'gems' do
# To be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/299263 authorize_upload!(user_project)
not_found! bad_request!('File is too large') if user_project.actual_limits.exceeded?(:rubygems_max_file_size, params[:file].size)
track_package_event('push_package', :rubygems)
ActiveRecord::Base.transaction do
package = ::Packages::CreateTemporaryPackageService.new(
user_project, current_user, declared_params.merge(build: current_authenticated_job)
).execute(:rubygems, name: ::Packages::Rubygems::TEMPORARY_PACKAGE_NAME)
file_params = {
file: params[:file],
file_name: PACKAGE_FILENAME
}
::Packages::CreatePackageFileService.new(
package, file_params.merge(build: current_authenticated_job)
).execute
end
created!
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: user_project.id })
forbidden!
end end
desc 'Fetch a list of dependencies' do desc 'Fetch a list of dependencies' do
......
...@@ -41,6 +41,9 @@ ...@@ -41,6 +41,9 @@
- i_package_pypi_delete_package - i_package_pypi_delete_package
- i_package_pypi_pull_package - i_package_pypi_pull_package
- i_package_pypi_push_package - i_package_pypi_push_package
- i_package_rubygems_delete_package
- i_package_rubygems_pull_package
- i_package_rubygems_push_package
- i_package_tag_delete_package - i_package_tag_delete_package
- i_package_tag_pull_package - i_package_tag_pull_package
- i_package_tag_push_package - i_package_tag_push_package
...@@ -99,6 +99,16 @@ ...@@ -99,6 +99,16 @@
aggregation: weekly aggregation: weekly
redis_slot: package redis_slot: package
feature_flag: collect_package_events_redis feature_flag: collect_package_events_redis
- name: i_package_rubygems_deploy_token
category: deploy_token_packages
aggregation: weekly
redis_slot: package
feature_flag: collect_package_events_redis
- name: i_package_rubygems_user
category: user_packages
aggregation: weekly
redis_slot: package
feature_flag: collect_package_events_redis
- name: i_package_tag_deploy_token - name: i_package_tag_deploy_token
category: deploy_token_packages category: deploy_token_packages
aggregation: weekly aggregation: weekly
......
...@@ -122,7 +122,7 @@ RSpec.describe Packages::GroupPackagesFinder do ...@@ -122,7 +122,7 @@ RSpec.describe Packages::GroupPackagesFinder do
end end
context 'when there are processing packages' do context 'when there are processing packages' do
let_it_be(:package4) { create(:nuget_package, project: project, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } let_it_be(:package4) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
it { is_expected.to match_array([package1, package2]) } it { is_expected.to match_array([package1, package2]) }
end end
......
...@@ -14,7 +14,7 @@ RSpec.describe ::Packages::PackageFinder do ...@@ -14,7 +14,7 @@ RSpec.describe ::Packages::PackageFinder do
it { is_expected.to eq(maven_package) } it { is_expected.to eq(maven_package) }
context 'processing packages' do context 'processing packages' do
let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
let(:package_id) { nuget_package.id } let(:package_id) { nuget_package.id }
it 'are not returned' do it 'are not returned' do
......
...@@ -76,7 +76,7 @@ RSpec.describe ::Packages::PackagesFinder do ...@@ -76,7 +76,7 @@ RSpec.describe ::Packages::PackagesFinder do
end end
context 'with processing packages' do context 'with processing packages' do
let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
it { is_expected.to match_array([conan_package, maven_package]) } it { is_expected.to match_array([conan_package, maven_package]) }
end end
......
...@@ -14,7 +14,7 @@ RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_red ...@@ -14,7 +14,7 @@ RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_red
end end
it 'includes the right events' do it 'includes the right events' do
expect(described_class::KNOWN_EVENTS.size).to eq 45 expect(described_class::KNOWN_EVENTS.size).to eq 48
end end
described_class::KNOWN_EVENTS.each do |event| described_class::KNOWN_EVENTS.each do |event|
......
...@@ -511,7 +511,7 @@ RSpec.describe Packages::Package, type: :model do ...@@ -511,7 +511,7 @@ RSpec.describe Packages::Package, type: :model do
describe '.without_nuget_temporary_name' do describe '.without_nuget_temporary_name' do
let!(:package1) { create(:nuget_package) } let!(:package1) { create(:nuget_package) }
let!(:package2) { create(:nuget_package, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } let!(:package2) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
subject { described_class.without_nuget_temporary_name } subject { described_class.without_nuget_temporary_name }
...@@ -530,7 +530,7 @@ RSpec.describe Packages::Package, type: :model do ...@@ -530,7 +530,7 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to match_array([package1, package2, package3]) } it { is_expected.to match_array([package1, package2, package3]) }
context 'with temporary packages' do context 'with temporary packages' do
let!(:package1) { create(:nuget_package, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } let!(:package1) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
it { is_expected.to match_array([package2, package3]) } it { is_expected.to match_array([package2, package3]) }
end end
......
This diff is collapsed.
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
RSpec.describe Packages::Nuget::CreatePackageService do RSpec.describe Packages::CreateTemporaryPackageService do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:params) { {} } let_it_be(:params) { {} }
let_it_be(:package_name) { 'my-package' }
let_it_be(:package_type) { 'rubygems' }
describe '#execute' do describe '#execute' do
subject { described_class.new(project, user, params).execute } subject { described_class.new(project, user, params).execute(package_type, name: package_name) }
let(:package) { Packages::Package.last } let(:package) { Packages::Package.last }
it 'creates the package' do it 'creates the package', :aggregate_failures do
expect { subject }.to change { Packages::Package.count }.by(1) expect { subject }.to change { Packages::Package.count }.by(1)
expect(package).to be_valid expect(package).to be_valid
expect(package.name).to eq(Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) expect(package).to be_processing
expect(package.version).to start_with(Packages::Nuget::CreatePackageService::PACKAGE_VERSION) expect(package.name).to eq(package_name)
expect(package.package_type).to eq('nuget') expect(package.version).to start_with(described_class::PACKAGE_VERSION)
expect(package.package_type).to eq(package_type)
end end
it 'can create two packages in a row' do it 'can create two packages in a row', :aggregate_failures do
expect { subject }.to change { Packages::Package.count }.by(1) expect { subject }.to change { Packages::Package.count }.by(1)
expect { described_class.new(project, user, params).execute }.to change { Packages::Package.count }.by(1)
expect do
described_class.new(project, user, params).execute(package_type, name: package_name)
end.to change { Packages::Package.count }.by(1)
expect(package).to be_valid expect(package).to be_valid
expect(package.name).to eq(Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) expect(package).to be_processing
expect(package.version).to start_with(Packages::Nuget::CreatePackageService::PACKAGE_VERSION) expect(package.name).to eq(package_name)
expect(package.package_type).to eq('nuget') expect(package.version).to start_with(described_class::PACKAGE_VERSION)
expect(package.package_type).to eq(package_type)
end end
it_behaves_like 'assigns the package creator' it_behaves_like 'assigns the package creator'
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
end end
end end
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
let(:package) { create(:nuget_package) } let(:package) { create(:nuget_package, :processing) }
let(:package_file) { package.package_files.first } let(:package_file) { package.package_files.first }
let(:service) { described_class.new(package_file) } let(:service) { described_class.new(package_file) }
let(:package_name) { 'DummyProject.DummyPackage' } let(:package_name) { 'DummyProject.DummyPackage' }
...@@ -60,6 +60,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ ...@@ -60,6 +60,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
.to change { ::Packages::Package.count }.by(0) .to change { ::Packages::Package.count }.by(0)
.and change { Packages::DependencyLink.count }.by(0) .and change { Packages::DependencyLink.count }.by(0)
expect(package_file.reload.file_name).not_to eq(package_file_name) expect(package_file.reload.file_name).not_to eq(package_file_name)
expect(package_file.package).to be_processing
expect(package_file.package.reload.name).not_to eq(package_name) expect(package_file.package.reload.name).not_to eq(package_name)
expect(package_file.package.version).not_to eq(package_version) expect(package_file.package.version).not_to eq(package_version)
end end
...@@ -78,6 +79,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ ...@@ -78,6 +79,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
expect(package.reload.name).to eq(package_name) expect(package.reload.name).to eq(package_name)
expect(package.version).to eq(package_version) expect(package.version).to eq(package_version)
expect(package).to be_default
expect(package_file.reload.file_name).to eq(package_file_name) expect(package_file.reload.file_name).to eq(package_file_name)
# hard reset needed to properly reload package_file.file # hard reset needed to properly reload package_file.file
expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0 expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0
...@@ -184,6 +186,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ ...@@ -184,6 +186,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
expect(package.reload.name).to eq(package_name) expect(package.reload.name).to eq(package_name)
expect(package.version).to eq(package_version) expect(package.version).to eq(package_version)
expect(package).to be_default
expect(package_file.reload.file_name).to eq(package_file_name) expect(package_file.reload.file_name).to eq(package_file_name)
# hard reset needed to properly reload package_file.file # hard reset needed to properly reload package_file.file
expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0 expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0
......
# frozen_string_literal: true
RSpec.shared_examples 'rejects rubygems packages access' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
end
end
RSpec.shared_examples 'process rubygems workhorse authorization' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
it 'has the proper content type' do
subject
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
context 'with a request that bypassed gitlab-workhorse' do
let(:headers) do
{ 'HTTP_AUTHORIZATION' => personal_access_token.token }
.merge(workhorse_headers)
.tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
end
before do
project.add_maintainer(user)
end
it_behaves_like 'returning response status', :forbidden
end
end
end
RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_member = true|
RSpec.shared_examples 'creates rubygems package files' do
it 'creates package files', :aggregate_failures do
expect { subject }
.to change { project.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(status)
package_file = project.packages.last.package_files.reload.last
expect(package_file.file_name).to eq('package.gem')
end
end
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
context 'with object storage disabled' do
before do
stub_package_file_object_storage(enabled: false)
end
context 'without a file from workhorse' do
let(:send_rewritten_field) { false }
it_behaves_like 'returning response status', :bad_request
end
context 'with correct params' do
it_behaves_like 'package workhorse uploads'
it_behaves_like 'creates rubygems package files'
it_behaves_like 'a package tracking event', 'API::RubygemPackages', 'push_package'
end
end
context 'with object storage enabled' do
let(:tmp_object) do
fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
key: "tmp/uploads/#{file_name}",
body: 'content'
)
end
let(:fog_file) { fog_to_uploaded_file(tmp_object) }
let(:params) { { file: fog_file, 'file.remote_id' => file_name } }
context 'and direct upload enabled' do
let(:fog_connection) do
stub_package_file_object_storage(direct_upload: true)
end
it_behaves_like 'creates rubygems package files'
['123123', '../../123123'].each do |remote_id|
context "with invalid remote_id: #{remote_id}" do
let(:params) do
{
file: fog_file,
'file.remote_id' => remote_id
}
end
it_behaves_like 'returning response status', :forbidden
end
end
end
context 'and direct upload disabled' do
context 'and background upload disabled' do
let(:fog_connection) do
stub_package_file_object_storage(direct_upload: false, background_upload: false)
end
it_behaves_like 'creates rubygems package files'
end
context 'and background upload enabled' do
let(:fog_connection) do
stub_package_file_object_storage(direct_upload: false, background_upload: true)
end
it_behaves_like 'creates rubygems package files'
end
end
end
end
end
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