Commit bde1e878 authored by Andreas Brandl's avatar Andreas Brandl

Merge branch '13345-conan-file-metadatum' into 'master'

Conan snapshot and manifest API endpoints

See merge request gitlab-org/gitlab!16418
parents 16020d79 16775e71
# frozen_string_literal: true
class CreatePackagesConanFileMetadata < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :packages_conan_file_metadata do |t|
t.references :package_file, index: { unique: true }, null: false, foreign_key: { to_table: :packages_package_files, on_delete: :cascade }, type: :bigint
t.timestamps_with_timezone
t.string "recipe_revision", null: false, default: "0", limit: 255
t.string "package_revision", limit: 255
t.string "conan_package_reference", limit: 255
t.integer "conan_file_type", limit: 2, null: false
end
end
end
# frozen_string_literal: true
class CreatePackagesConanMetadata < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :packages_conan_metadata do |t|
t.references :package, index: { unique: true }, null: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
t.timestamps_with_timezone
t.string "package_username", null: false, limit: 255
t.string "package_channel", null: false, limit: 255
end
end
end
...@@ -2653,6 +2653,26 @@ ActiveRecord::Schema.define(version: 2019_10_29_191901) do ...@@ -2653,6 +2653,26 @@ ActiveRecord::Schema.define(version: 2019_10_29_191901) do
t.index ["project_id", "token_encrypted"], name: "index_feature_flags_clients_on_project_id_and_token_encrypted", unique: true t.index ["project_id", "token_encrypted"], name: "index_feature_flags_clients_on_project_id_and_token_encrypted", unique: true
end end
create_table "packages_conan_file_metadata", force: :cascade do |t|
t.bigint "package_file_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "recipe_revision", limit: 255, default: "0", null: false
t.string "package_revision", limit: 255
t.string "conan_package_reference", limit: 255
t.integer "conan_file_type", limit: 2, null: false
t.index ["package_file_id"], name: "index_packages_conan_file_metadata_on_package_file_id", unique: true
end
create_table "packages_conan_metadata", force: :cascade do |t|
t.bigint "package_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "package_username", limit: 255, null: false
t.string "package_channel", limit: 255, null: false
t.index ["package_id"], name: "index_packages_conan_metadata_on_package_id", unique: true
end
create_table "packages_maven_metadata", force: :cascade do |t| create_table "packages_maven_metadata", force: :cascade do |t|
t.bigint "package_id", null: false t.bigint "package_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
...@@ -4326,6 +4346,8 @@ ActiveRecord::Schema.define(version: 2019_10_29_191901) do ...@@ -4326,6 +4346,8 @@ ActiveRecord::Schema.define(version: 2019_10_29_191901) do
add_foreign_key "operations_feature_flag_scopes", "operations_feature_flags", column: "feature_flag_id", on_delete: :cascade add_foreign_key "operations_feature_flag_scopes", "operations_feature_flags", column: "feature_flag_id", on_delete: :cascade
add_foreign_key "operations_feature_flags", "projects", on_delete: :cascade add_foreign_key "operations_feature_flags", "projects", on_delete: :cascade
add_foreign_key "operations_feature_flags_clients", "projects", on_delete: :cascade add_foreign_key "operations_feature_flags_clients", "projects", on_delete: :cascade
add_foreign_key "packages_conan_file_metadata", "packages_package_files", column: "package_file_id", on_delete: :cascade
add_foreign_key "packages_conan_metadata", "packages_packages", column: "package_id", on_delete: :cascade
add_foreign_key "packages_maven_metadata", "packages_packages", column: "package_id", name: "fk_be88aed360", on_delete: :cascade add_foreign_key "packages_maven_metadata", "packages_packages", column: "package_id", name: "fk_be88aed360", on_delete: :cascade
add_foreign_key "packages_package_files", "packages_packages", column: "package_id", name: "fk_86f0f182f8", on_delete: :cascade add_foreign_key "packages_package_files", "packages_packages", column: "package_id", name: "fk_86f0f182f8", on_delete: :cascade
add_foreign_key "packages_package_metadata", "packages_packages", column: "package_id", on_delete: :cascade add_foreign_key "packages_package_metadata", "packages_packages", column: "package_id", on_delete: :cascade
......
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
module Packages module Packages
class ConanPackageFinder class ConanPackageFinder
attr_reader :query, :current_user attr_reader :current_user, :query
def initialize(query, current_user) def initialize(current_user, params)
@query = query
@current_user = current_user @current_user = current_user
@query = params[:query]
end end
def execute def execute
packages_for_current_user.with_name_like(query).order_name_asc packages_for_current_user.with_name_like(query).order_name_asc if query
end end
private private
......
# frozen_string_literal: true
class Packages::ConanFileMetadatum < ApplicationRecord
belongs_to :package_file, inverse_of: :conan_file_metadatum
validates :package_file, presence: true
validates :recipe_revision,
presence: true,
format: { with: Gitlab::Regex.conan_revision_regex }
validates :package_revision, absence: true, if: :recipe_file?
validates :package_revision, format: { with: Gitlab::Regex.conan_revision_regex }, if: :package_file?
validates :conan_package_reference, absence: true, if: :recipe_file?
validates :conan_package_reference, format: { with: Gitlab::Regex.conan_package_reference_regex }, if: :package_file?
enum conan_file_type: { recipe_file: 1, package_file: 2 }
RECIPE_FILES = %w[conanfile.py conanmanifest.txt].freeze
PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
end
# frozen_string_literal: true
class Packages::ConanMetadatum < ApplicationRecord
belongs_to :package, inverse_of: :conan_metadatum
validates :package, presence: true
validates :package_username,
presence: true,
format: { with: Gitlab::Regex.conan_recipe_component_regex }
validates :package_channel,
presence: true,
format: { with: Gitlab::Regex.conan_recipe_component_regex }
def recipe
"#{package.name}/#{package.version}@#{package_username}/#{package_channel}"
end
def recipe_path
recipe.tr('@', '/')
end
def self.package_username_from(full_path:)
full_path.tr('/', '+')
end
def self.full_path_from(package_username:)
package_username.tr('+', '/')
end
end
...@@ -5,10 +5,14 @@ class Packages::Package < ApplicationRecord ...@@ -5,10 +5,14 @@ class Packages::Package < ApplicationRecord
belongs_to :project belongs_to :project
# package_files must be destroyed by ruby code in order to properly remove carrierwave uploads and update project statistics # package_files must be destroyed by ruby code in order to properly remove carrierwave uploads and update project statistics
has_many :package_files, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :package_files, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :conan_metadatum, inverse_of: :package
has_one :maven_metadatum, inverse_of: :package has_one :maven_metadatum, inverse_of: :package
accepts_nested_attributes_for :conan_metadatum
accepts_nested_attributes_for :maven_metadatum accepts_nested_attributes_for :maven_metadatum
delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan
validates :project, presence: true validates :project, presence: true
validates :name, validates :name,
......
...@@ -8,12 +8,17 @@ class Packages::PackageFile < ApplicationRecord ...@@ -8,12 +8,17 @@ class Packages::PackageFile < ApplicationRecord
belongs_to :package belongs_to :package
has_one :conan_file_metadatum, inverse_of: :package_file
accepts_nested_attributes_for :conan_file_metadatum
validates :package, presence: true validates :package, presence: true
validates :file, presence: true validates :file, presence: true
validates :file_name, presence: true validates :file_name, presence: true
scope :recent, -> { order(id: :desc) } scope :recent, -> { order(id: :desc) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) } scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :with_conan_file_metadata, -> { includes(:conan_file_metadatum) }
mount_uploader :file, Packages::PackageFileUploader mount_uploader :file, Packages::PackageFileUploader
......
# frozen_string_literal: true
class ConanPackagePresenter
include API::Helpers::RelatedResourcesHelpers
include Gitlab::Utils::StrongMemoize
def initialize(recipe, user, project)
@recipe = recipe
@user = user
@project = project
end
def recipe_urls
map_package_files do |package_file|
build_recipe_file_url(package_file) if package_file.conan_file_metadatum.recipe_file?
end
end
def recipe_snapshot
map_package_files do |package_file|
package_file.file_md5 if package_file.conan_file_metadatum.recipe_file?
end
end
def package_urls
map_package_files do |package_file|
build_package_file_url(package_file) if package_file.conan_file_metadatum.package_file?
end
end
def package_snapshot
map_package_files do |package_file|
package_file.file_md5 if package_file.conan_file_metadatum.package_file?
end
end
private
def build_recipe_file_url(package_file)
expose_url(
api_v4_packages_conan_v1_files_export_path(
package_name: package.name,
package_version: package.version,
package_username: package.conan_metadatum.package_username,
package_channel: package.conan_metadatum.package_channel,
recipe_revision: package_file.conan_file_metadatum.recipe_revision,
file_name: package_file.file_name
)
)
end
def build_package_file_url(package_file)
expose_url(
api_v4_packages_conan_v1_files_package_path(
package_name: package.name,
package_version: package.version,
package_username: package.conan_metadatum.package_username,
package_channel: package.conan_metadatum.package_channel,
recipe_revision: package_file.conan_file_metadatum.recipe_revision,
conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
package_revision: package_file.conan_file_metadatum.package_revision,
file_name: package_file.file_name
)
)
end
def map_package_files
package_files.to_a.map do |package_file|
[package_file.file_name, yield(package_file)]
end.to_h.compact
end
def package_files
return unless package
@package_files ||= package.package_files.with_conan_file_metadata
end
def package
strong_memoize(:package) do
name, version = @recipe.split('@')[0].split('/')
@project.packages.with_name(name).with_version(version).order_created.last
end
end
end
...@@ -23,6 +23,8 @@ module Packages ...@@ -23,6 +23,8 @@ module Packages
def search_results def search_results
return [] if wildcard_query? return [] if wildcard_query?
return search_for_single_package(sanitized_query) if params[:query].include?(RECIPE_SEPARATOR)
search_packages(build_query) search_packages(build_query)
end end
...@@ -31,11 +33,9 @@ module Packages ...@@ -31,11 +33,9 @@ module Packages
end end
def build_query def build_query
sanitized_query = sanitize_sql_like(params[:query].delete(WILDCARD))
return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD) return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
return sanitized_query if sanitized_query.include?(RECIPE_SEPARATOR)
"#{sanitized_query}/%" sanitized_query
end end
def feature_available? def feature_available?
...@@ -43,7 +43,21 @@ module Packages ...@@ -43,7 +43,21 @@ module Packages
end end
def search_packages(query) def search_packages(query)
Packages::ConanPackageFinder.new(query, current_user).execute.pluck_names Packages::ConanPackageFinder.new(current_user, query: query).execute.map(&:conan_recipe)
end
def search_for_single_package(query)
name, version, username, _ = query.split(/[@\/]/)
full_path = Packages::ConanMetadatum.full_path_from(package_username: username)
project = Project.find_by_full_path(full_path)
return unless current_user.can?(:read_package, project)
result = project.packages.with_name(name).with_version(version).order_created.last
[result&.conan_recipe].compact
end
def sanitized_query
@sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
end end
end end
end end
......
...@@ -4,16 +4,48 @@ module API ...@@ -4,16 +4,48 @@ module API
class ConanPackages < Grape::API class ConanPackages < Grape::API
helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::PackagesHelpers
PACKAGE_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX,
package_version: API::NO_SLASH_URL_PART_REGEX,
package_username: API::NO_SLASH_URL_PART_REGEX,
package_channel: API::NO_SLASH_URL_PART_REGEX
}.freeze
FILE_NAME_REQUIREMENTS = {
file_name: Gitlab::Regex.conan_file_name_regex
}.freeze
PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex
before do before do
not_found! unless Feature.enabled?(:conan_package_registry) not_found! unless Feature.enabled?(:conan_package_registry)
require_packages_enabled! require_packages_enabled!
# Personal access token will be extracted from Bearer or Basic authorization # Personal access token will be extracted from Bearer or Basic authorization
# in the overriden find_personal_access_token helper # in the overridden find_personal_access_token helper
authenticate! authenticate!
end end
namespace 'packages/conan/v1/users/' do namespace 'packages/conan/v1' do
desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2'
end
get 'ping' do
header 'X-Conan-Server-Capabilities', [].join(',')
end
desc 'Search for packages' do
detail 'This feature was introduced in GitLab 12.4'
end
params do
requires :q, type: String, desc: 'Search query'
end
get 'conans/search' do
service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
service.payload
end
namespace 'users' do
format :txt format :txt
desc 'Authenticate user against conan CLI' do desc 'Authenticate user against conan CLI' do
...@@ -25,7 +57,7 @@ module API ...@@ -25,7 +57,7 @@ module API
end end
desc 'Check for valid user credentials per conan CLI' do desc 'Check for valid user credentials per conan CLI' do
detail 'This feature was introduced in GitLab 12.3' detail 'This feature was introduced in GitLab 12.4'
end end
get 'check_credentials' do get 'check_credentials' do
authenticate! authenticate!
...@@ -33,51 +65,69 @@ module API ...@@ -33,51 +65,69 @@ module API
end end
end end
namespace 'packages/conan/v1/' do
desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2'
end
get 'ping' do
header 'X-Conan-Server-Capabilities', [].join(',')
end
desc 'Search for packages' do
detail 'This feature was introduced in GitLab 12.3'
end
params do params do
requires :q, type: String, desc: 'Search query' requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
end end
get 'conans/search' do namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do
service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute # Get the snapshot
service.payload #
# the snapshot is a hash of { filename: md5 hash }
# md5 hash is the has of that file. This hash is used to diff the files existing on the client
# to determine which client files need to be uploaded if no recipe exists the snapshot is empty
desc 'Package Snapshot' do
detail 'This feature was introduced in GitLab 12.5'
end end
get 'packages/:conan_package_reference' do
authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
present presenter, with: EE::API::Entities::ConanPackage::ConanPackageSnapshot
end end
namespace 'packages/conan/v1/conans/*url_recipe' do desc 'Recipe Snapshot' do
before do detail 'This feature was introduced in GitLab 12.5'
render_api_error!("Invalid recipe", 400) unless valid_url_recipe?(params[:url_recipe])
end end
params do get do
requires :url_recipe, type: String, desc: 'Package recipe' authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
present presenter, with: EE::API::Entities::ConanPackage::ConanRecipeSnapshot
end end
# Get the recipe manifest # Get the manifest
# returns the download urls for the existing recipe in the registry # returns the download urls for the existing recipe in the registry
# #
# the manifest is a hash of { filename: url } # the manifest is a hash of { filename: url }
# where the url is the download url for the file # where the url is the download url for the file
desc 'Package Digest' do desc 'Package Digest' do
detail 'This feature was introduced in GitLab 12.3' detail 'This feature was introduced in GitLab 12.5'
end end
get 'packages/:package_id/digest' do get 'packages/:conan_package_reference/digest' do
render_api_error!("No recipe manifest found", 404) authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
render_api_error!("No recipe manifest found", 404) if presenter.package_urls.empty?
present presenter, with: EE::API::Entities::ConanPackage::ConanPackageManifest
end end
desc 'Recipe Digest' do desc 'Recipe Digest' do
detail 'This feature was introduced in GitLab 12.3' detail 'This feature was introduced in GitLab 12.5'
end end
get 'digest' do get 'digest' do
render_api_error!("No recipe manifest found", 404) authorize!(:read_package, project)
presenter = ConanPackagePresenter.new(recipe, current_user, project)
render_api_error!("No recipe manifest found", 404) if presenter.recipe_urls.empty?
present presenter, with: EE::API::Entities::ConanPackage::ConanRecipeManifest
end end
# Get the upload urls # Get the upload urls
...@@ -88,54 +138,126 @@ module API ...@@ -88,54 +138,126 @@ module API
# returns { filename: url } # returns { filename: url }
# where the url is the upload url for the file that the conan client will use # where the url is the upload url for the file that the conan client will use
desc 'Package Upload Urls' do desc 'Package Upload Urls' do
detail 'This feature was introduced in GitLab 12.3' detail 'This feature was introduced in GitLab 12.4'
end end
params do params do
requires :package_id, type: String, desc: 'Conan package ID' requires :conan_package_reference, type: String, desc: 'Conan package ID'
end end
post 'packages/:package_id/upload_urls' do post 'packages/:conan_package_reference/upload_urls' do
authorize!(:read_package, project)
status 200 status 200
{ upload_urls = package_upload_urls(::Packages::ConanFileMetadatum::PACKAGE_FILES)
'conaninfo.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conaninfo.txt",
'conanmanifest.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conanmanifest.txt", present upload_urls, with: EE::API::Entities::ConanPackage::ConanUploadUrls
'conan_package.tgz': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conan_package.tgz"
}
end end
desc 'Recipe Upload Urls' do desc 'Recipe Upload Urls' do
detail 'This feature was introduced in GitLab 12.3' detail 'This feature was introduced in GitLab 12.4'
end end
post 'upload_urls' do post 'upload_urls' do
authorize!(:read_package, project)
status 200 status 200
{ upload_urls = recipe_upload_urls(::Packages::ConanFileMetadatum::RECIPE_FILES)
'conanfile.py': "#{base_file_url}/#{params[:url_recipe]}/-/0/export/conanfile.py",
'conanmanifest.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/export/conanmanifest.txt" present upload_urls, with: EE::API::Entities::ConanPackage::ConanUploadUrls
} end
end end
# Get the recipe snapshot params do
# requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
# the snapshot is a hash of { filename: md5 hash } requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
# md5 hash is the has of that file. This hash is used to diff the files existing on the client requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
# to determine which client files need to be uploaded if no recipe exists the snapshot is empty requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
desc 'Recipe Snapshot' do requires :recipe_revision, type: String, desc: 'Conan Recipe Revision'
detail 'This feature was introduced in GitLab 12.3'
end end
get '/' do namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision' do
{} before do
authenticate_non_get!
end end
desc 'Package Snapshot' do params do
detail 'This feature was introduced in GitLab 12.3' requires :file_name, type: String, desc: 'Package file name'
end
desc 'Download recipe files' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'export/:file_name' do
not_found!
end
params do
requires :conan_package_reference, type: String, desc: 'Conan Package ID'
requires :package_revision, type: String, desc: 'Conan Package Revision'
requires :file_name, type: String, desc: 'Package file name'
end
desc 'Download package files' do
detail 'This feature was introduced in GitLab 12.5'
end
get 'package/:conan_package_reference/:package_revision/:file_name' do
not_found!
end end
get 'packages/:package_id' do
{}
end end
end end
helpers do helpers do
def base_file_url include Gitlab::Utils::StrongMemoize
"#{::Settings.gitlab.base_url}/api/v4/packages/conan/v1/files" include ::API::Helpers::RelatedResourcesHelpers
def recipe_upload_urls(file_names)
{ upload_urls: Hash[
file_names.collect do |file_name|
[file_name, recipe_file_upload_url(file_name)]
end
] }
end
def package_upload_urls(file_names)
{ upload_urls: Hash[
file_names.collect do |file_name|
[file_name, package_file_upload_url(file_name)]
end
] }
end
def package_file_upload_url(file_name)
expose_url(
api_v4_packages_conan_v1_files_package_path(
package_name: params[:package_name],
package_version: params[:package_version],
package_username: params[:package_username],
package_channel: params[:package_channel],
recipe_revision: '0',
conan_package_reference: params[:conan_package_reference],
package_revision: '0',
file_name: file_name
)
)
end
def recipe_file_upload_url(file_name)
expose_url(
api_v4_packages_conan_v1_files_export_path(
package_name: params[:package_name],
package_version: params[:package_version],
package_username: params[:package_username],
package_channel: params[:package_channel],
recipe_revision: '0',
file_name: file_name
)
)
end
def recipe
"%{package_name}/%{package_version}@%{package_username}/%{package_channel}" % params.symbolize_keys
end
def project
strong_memoize(:project) do
full_path = ::Packages::ConanMetadatum.full_path_from(package_username: params[:package_username])
Project.find_by_full_path(full_path)
end
end end
def find_personal_access_token def find_personal_access_token
...@@ -167,10 +289,6 @@ module API ...@@ -167,10 +289,6 @@ module API
PersonalAccessToken.find_by_token(token) PersonalAccessToken.find_by_token(token)
end end
def valid_url_recipe?(recipe_url)
recipe_url =~ %r{\A(([\w](\.|\+|-)?)*(\/?)){4}\z}
end
end end
end end
end end
...@@ -808,6 +808,28 @@ module EE ...@@ -808,6 +808,28 @@ module EE
end end
end end
module ConanPackage
class ConanPackageManifest < Grape::Entity
expose :package_urls, merge: true
end
class ConanPackageSnapshot < Grape::Entity
expose :package_snapshot, merge: true
end
class ConanRecipeManifest < Grape::Entity
expose :recipe_urls, merge: true
end
class ConanRecipeSnapshot < Grape::Entity
expose :recipe_snapshot, merge: true
end
class ConanUploadUrls < Grape::Entity
expose :upload_urls, merge: true
end
end
class NpmPackage < Grape::Entity class NpmPackage < Grape::Entity
expose :name expose :name
expose :versions expose :versions
......
...@@ -6,6 +6,23 @@ module EE ...@@ -6,6 +6,23 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do class_methods do
def conan_file_name_regex
@conan_file_name_regex ||=
%r{\A#{(::Packages::ConanFileMetadatum::RECIPE_FILES + ::Packages::ConanFileMetadatum::PACKAGE_FILES).join("|")}\z}.freeze
end
def conan_package_reference_regex
@conan_package_reference_regex ||= %r{\A[A-Za-z0-9]+\z}.freeze
end
def conan_revision_regex
@conan_revision_regex ||= %r{\A0\z}.freeze
end
def conan_recipe_component_regex
@conan_recipe_component_regex ||= %r{\A(\w[.+-]?)+\z}.freeze
end
def package_name_regex def package_name_regex
@package_name_regex ||= %r{\A\@?(([\w\-\.\+]*)\/)*([\w\-\.]+)@?(([\w\-\.\+]*)\/)*([\w\-\.]*)\z}.freeze @package_name_regex ||= %r{\A\@?(([\w\-\.\+]*)\/)*([\w\-\.]+)@?(([\w\-\.\+]*)\/)*([\w\-\.]*)\z}.freeze
end end
......
...@@ -31,15 +31,98 @@ FactoryBot.define do ...@@ -31,15 +31,98 @@ FactoryBot.define do
end end
factory :conan_package do factory :conan_package do
sequence(:name) { |n| "package-#{n}/1.0.0@#{project.full_path.tr('/', '+')}/stable"} conan_metadatum
after :build do |package|
package.conan_metadatum.package_username = Packages::ConanMetadatum.package_username_from(
full_path: package.project.full_path
)
end
sequence(:name) { |n| "package-#{n}" }
version { '1.0.0' } version { '1.0.0' }
package_type { 'conan' } package_type { 'conan' }
after :create do |package|
create :conan_package_file, :conan_recipe_file, package: package
create :conan_package_file, :conan_recipe_manifest, package: package
create :conan_package_file, :conan_package_info, package: package
create :conan_package_file, :conan_package_manifest, package: package
create :conan_package_file, :conan_package, package: package
end
end end
end end
factory :package_file, class: Packages::PackageFile do factory :package_file, class: Packages::PackageFile do
package package
factory :conan_package_file do
trait(:conan_recipe_file) do
after :create do |package_file|
create :conan_file_metadatum, :recipe_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/recipe_conanfile.py') }
file_name { 'recipe_conanfile.py' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'py' }
size { 400.kilobytes }
end
trait(:conan_recipe_manifest) do
after :create do |package_file|
create :conan_file_metadatum, :recipe_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/recipe_conanmanifest.txt') }
file_name { 'recipe_conanmanifest.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'txt' }
size { 400.kilobytes }
end
trait(:conan_package_manifest) do
after :create do |package_file|
create :conan_file_metadatum, :package_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/package_conanmanifest.txt') }
file_name { 'package_conanmanifest.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'txt' }
size { 400.kilobytes }
end
trait(:conan_package_info) do
after :create do |package_file|
create :conan_file_metadatum, :package_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/package_conaninfo.txt') }
file_name { 'package_conaninfo.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'txt' }
size { 400.kilobytes }
end
trait(:conan_package) do
after :create do |package_file|
create :conan_file_metadatum, :package_file, package_file: package_file
end
file { fixture_file_upload('ee/spec/fixtures/conan/conan_package.tgz') }
file_name { 'conan_package.tgz' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' }
file_type { 'tgz' }
size { 400.kilobytes }
end
end
trait(:jar) do trait(:jar) do
file { fixture_file_upload('ee/spec/fixtures/maven/my-app-1.0-20180724.124855-1.jar') } file { fixture_file_upload('ee/spec/fixtures/maven/my-app-1.0-20180724.124855-1.jar') }
file_name { 'my-app-1.0-20180724.124855-1.jar' } file_name { 'my-app-1.0-20180724.124855-1.jar' }
...@@ -84,4 +167,25 @@ FactoryBot.define do ...@@ -84,4 +167,25 @@ FactoryBot.define do
app_name { 'my-app' } app_name { 'my-app' }
app_version { '1.0-SNAPSHOT' } app_version { '1.0-SNAPSHOT' }
end end
factory :conan_metadatum, class: Packages::ConanMetadatum do
package
package_username { 'username' }
package_channel { 'stable' }
end
factory :conan_file_metadatum, class: Packages::ConanFileMetadatum do
package_file
recipe_revision { '0' }
trait(:recipe_file) do
conan_file_type { 'recipe_file' }
end
trait(:package_file) do
conan_file_type { 'package_file' }
package_revision { '0' }
conan_package_reference { '123456789' }
end
end
end end
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
require 'spec_helper' require 'spec_helper'
describe Packages::ConanPackageFinder do describe Packages::ConanPackageFinder do
describe '#execute' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
describe '#execute' do
let!(:conan_package) { create(:conan_package, project: project) } let!(:conan_package) { create(:conan_package, project: project) }
let!(:conan_package2) { create(:conan_package, project: project) } let!(:conan_package2) { create(:conan_package, project: project) }
subject { described_class.new(query, user).execute } subject { described_class.new(user, query: query).execute }
context 'packages that are not visible to user' do context 'packages that are not visible to user' do
let!(:non_visible_project) { create(:project, :private) } let!(:non_visible_project) { create(:project, :private) }
......
[settings]
arch=x86_64
build_type=Release
compiler=apple-clang
compiler.libcxx=libc++
compiler.version=10.0
os=Macos
[requires]
[options]
shared=False
[full_settings]
arch=x86_64
build_type=Release
compiler=apple-clang
compiler.libcxx=libc++
compiler.version=10.0
os=Macos
[full_requires]
[full_options]
shared=False
[recipe_hash]
b4b91125b36b40a7076a98310588f820
[env]
1565723794
conaninfo.txt: 2774ebe649804c1cd9430f26ab0ead14
include/hello.h: 8727846905bd09baecf8bdc1edb1f46e
lib/libhello.a: 7f2aaa8b6f3bc316bba59e47b6a0bd43
from conans import ConanFile, CMake, tools
class HelloConan(ConanFile):
name = "Hello"
version = "0.1"
license = "<Put the package license here>"
author = "<Put your name here> <And your email here>"
url = "<Package recipe repository url here, for issues about the package>"
description = "<Description of Hello here>"
topics = ("<Put some tag here>", "<here>", "<and here>")
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False]}
default_options = "shared=False"
generators = "cmake"
def source(self):
self.run("git clone https://github.com/conan-io/hello.git")
# This small hack might be useful to guarantee proper /MT /MD linkage
# in MSVC if the packaged project doesn't have variables to set it
# properly
tools.replace_in_file("hello/CMakeLists.txt", "PROJECT(HelloWorld)",
'''PROJECT(HelloWorld)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()''')
def build(self):
cmake = CMake(self)
cmake.configure(source_folder="hello")
cmake.build()
# Explicit way:
# self.run('cmake %s/hello %s'
# % (self.source_folder, cmake.command_line))
# self.run("cmake --build . %s" % cmake.build_config)
def package(self):
self.copy("*.h", dst="include", src="hello")
self.copy("*hello.lib", dst="lib", keep_path=False)
self.copy("*.dll", dst="bin", keep_path=False)
self.copy("*.so", dst="lib", keep_path=False)
self.copy("*.dylib", dst="lib", keep_path=False)
self.copy("*.a", dst="lib", keep_path=False)
def package_info(self):
self.cpp_info.libs = ["hello"]
1565723790
conanfile.py: 7c042b95312cc4c4ee89199dc51aebf9
...@@ -3,6 +3,45 @@ ...@@ -3,6 +3,45 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Regex do describe Gitlab::Regex do
describe '.conan_file_name_regex' do
subject { described_class.conan_file_name_regex }
it { is_expected.to match('conanfile.py') }
it { is_expected.to match('conan_package.tgz') }
it { is_expected.not_to match('foo.txt') }
it { is_expected.not_to match('!!()()') }
end
describe '.conan_package_reference_regex' do
subject { described_class.conan_package_reference_regex }
it { is_expected.to match('123456789') }
it { is_expected.to match('asdf1234') }
it { is_expected.not_to match('@foo') }
it { is_expected.not_to match('0/pack+age/1@1/0') }
it { is_expected.not_to match('!!()()') }
end
describe '.conan_revision_regex' do
subject { described_class.conan_revision_regex }
it { is_expected.to match('0') }
it { is_expected.not_to match('foo') }
it { is_expected.not_to match('!!()()') }
end
describe '.conan_recipe_component_regex' do
subject { described_class.conan_recipe_component_regex }
it { is_expected.to match('foobar') }
it { is_expected.to match('foo_bar') }
it { is_expected.to match('foo+bar') }
it { is_expected.to match('1.0.0') }
it { is_expected.not_to match('foo@bar') }
it { is_expected.not_to match('foo/bar') }
it { is_expected.not_to match('!!()()') }
end
describe '.feature_flag_regex' do describe '.feature_flag_regex' do
subject { described_class.feature_flag_regex } subject { described_class.feature_flag_regex }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::ConanFileMetadatum, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package_file) }
end
describe 'validations' do
let(:package_file) do
create(:package_file,
file: fixture_file_upload('ee/spec/fixtures/conan/recipe_conanfile.py'),
file_name: 'recipe_conanfile.py')
end
it { is_expected.to validate_presence_of(:package_file) }
it { is_expected.to validate_presence_of(:recipe_revision) }
describe '#recipe_revision' do
it { is_expected.to allow_value("0").for(:recipe_revision) }
it { is_expected.not_to allow_value(nil).for(:recipe_revision) }
end
describe '#package_revision_for_package_file' do
context 'recipe file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
it 'is valid with empty value' do
conan_file_metadatum.package_revision = nil
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with value' do
conan_file_metadatum.package_revision = '0'
expect(conan_file_metadatum).to be_invalid
end
end
context 'package file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
it 'is valid with default value' do
conan_file_metadatum.package_revision = '0'
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with non-default value' do
conan_file_metadatum.package_revision = 'foo'
expect(conan_file_metadatum).to be_invalid
end
end
end
describe '#conan_package_reference_for_package_file' do
context 'recipe file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
it 'is valid with empty value' do
conan_file_metadatum.conan_package_reference = nil
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with value' do
conan_file_metadatum.conan_package_reference = '123456789'
expect(conan_file_metadatum).to be_invalid
end
end
context 'package file' do
let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
it 'is valid with acceptable value' do
conan_file_metadatum.conan_package_reference = '123456asdf'
expect(conan_file_metadatum).to be_valid
end
it 'is invalid with invalid value' do
conan_file_metadatum.conan_package_reference = 'foo@bar'
expect(conan_file_metadatum).to be_invalid
end
it 'is invalid when nil' do
conan_file_metadatum.conan_package_reference = nil
expect(conan_file_metadatum).to be_invalid
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::ConanMetadatum, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to validate_presence_of(:package_username) }
it { is_expected.to validate_presence_of(:package_channel) }
describe '#package_username' do
it { is_expected.to allow_value("my-package+username").for(:package_username) }
it { is_expected.not_to allow_value("my/package").for(:package_username) }
it { is_expected.not_to allow_value("my(package)").for(:package_username) }
it { is_expected.not_to allow_value("my@package").for(:package_username) }
end
describe '#package_channel' do
it { is_expected.to allow_value("beta").for(:package_channel) }
it { is_expected.to allow_value("stable+1.0").for(:package_channel) }
it { is_expected.not_to allow_value("my/channel").for(:package_channel) }
it { is_expected.not_to allow_value("my(channel)").for(:package_channel) }
it { is_expected.not_to allow_value("my@channel").for(:package_channel) }
end
end
describe '#recipe' do
let(:package) { create(:conan_package) }
it 'returns the recipe' do
expect(package.conan_recipe).to eq("#{package.name}/#{package.version}@#{package.conan_metadatum.package_username}/#{package.conan_metadatum.package_channel}")
end
end
describe '#recipe_url' do
let(:package) { create(:conan_package) }
it 'returns the recipe url' do
expect(package.conan_recipe_path).to eq("#{package.name}/#{package.version}/#{package.conan_metadatum.package_username}/#{package.conan_metadatum.package_channel}")
end
end
describe '.package_username_from' do
let(:full_path) { 'foo/bar/baz-buz' }
it 'returns the username formatted package path' do
expect(described_class.package_username_from(full_path: full_path)).to eq('foo+bar+baz-buz')
end
end
describe '.full_path_from' do
let(:username) { 'foo+bar+baz-buz' }
it 'returns the username formatted package path' do
expect(described_class.full_path_from(package_username: username)).to eq('foo/bar/baz-buz')
end
end
end
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Packages::PackageFile, type: :model do RSpec.describe Packages::PackageFile, type: :model do
describe 'relationships' do describe 'relationships' do
it { is_expected.to belong_to(:package) } it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
end end
describe 'validations' do describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe ConanPackagePresenter do
let(:user) { create(:user) }
let(:project) { create(:project) }
describe '#recipe_urls' do
subject { described_class.new(recipe, user, project).recipe_urls }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"recipe_conanfile.py" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/recipe_conanfile.py",
"recipe_conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/recipe_conanmanifest.txt"
}
end
it { is_expected.to eq(expected_result) }
end
end
describe '#recipe_snapshot' do
subject { described_class.new(recipe, user, project).recipe_snapshot }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"recipe_conanfile.py" => '12345abcde',
"recipe_conanmanifest.txt" => '12345abcde'
}
end
it { is_expected.to eq(expected_result) }
end
end
describe '#package_urls' do
subject { described_class.new(recipe, user, project).package_urls }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"package_conaninfo.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/package_conaninfo.txt",
"package_conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/package_conanmanifest.txt",
"conan_package.tgz" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
}
end
it { is_expected.to eq(expected_result) }
end
end
describe '#package_snapshot' do
subject { described_class.new(recipe, user, project).package_snapshot }
context 'no existing package' do
let(:recipe) { "my-pkg/v1.0.0/#{project.full_path}/stable" }
it { is_expected.to be_empty }
end
context 'existing package' do
let(:package) { create(:conan_package, project: project) }
let(:recipe) { package.conan_recipe }
let(:expected_result) do
{
"package_conaninfo.txt" => '12345abcde',
"package_conanmanifest.txt" => '12345abcde',
"conan_package.tgz" => '12345abcde'
}
end
it { is_expected.to eq(expected_result) }
end
end
end
...@@ -2,10 +2,16 @@ ...@@ -2,10 +2,16 @@
require 'spec_helper' require 'spec_helper'
describe API::ConanPackages do describe API::ConanPackages do
let_it_be(:package) { create(:conan_package) }
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
let(:project) { package.project }
let(:base_secret) { SecureRandom.base64(64) } let(:base_secret) { SecureRandom.base64(64) }
let(:personal_access_token) { create(:personal_access_token) } let(:auth_token) { personal_access_token.token }
let(:headers) do let(:headers) do
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', personal_access_token.token) } { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
end end
let(:jwt_secret) do let(:jwt_secret) do
...@@ -17,21 +23,11 @@ describe API::ConanPackages do ...@@ -17,21 +23,11 @@ describe API::ConanPackages do
end end
before do before do
project.add_developer(user)
stub_licensed_features(packages: true) stub_licensed_features(packages: true)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
describe 'GET /api/v4/packages/conan/v1/ping' do describe 'GET /api/v4/packages/conan/v1/ping' do
context 'feature flag disabled' do context 'feature flag disabled' do
before do before do
...@@ -99,48 +95,59 @@ describe API::ConanPackages do ...@@ -99,48 +95,59 @@ describe API::ConanPackages do
end end
describe 'GET /api/v4/packages/conan/v1/conans/search' do describe 'GET /api/v4/packages/conan/v1/conans/search' do
let(:project) { create(:project, :public) }
let(:package) { create(:conan_package, project: project) }
before do before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
get api('/packages/conan/v1/conans/search'), headers: headers, params: params get api('/packages/conan/v1/conans/search'), headers: headers, params: params
end end
subject { JSON.parse(response.body)['results'] } subject { json_response['results'] }
context 'returns packages with a matching name' do context 'returns packages with a matching name' do
let(:params) { { q: package.name } } let(:params) { { q: package.conan_recipe } }
it { is_expected.to contain_exactly(package.name) } it { is_expected.to contain_exactly(package.conan_recipe) }
end end
context 'returns packages using a * wildcard' do context 'returns packages using a * wildcard' do
let(:params) {{ q: "#{package.name[0, 3]}*" }} let(:params) { { q: "#{package.name[0, 3]}*" } }
it { is_expected.to contain_exactly(package.name) } it { is_expected.to contain_exactly(package.conan_recipe) }
end end
context 'does not return non-matching packages' do context 'does not return non-matching packages' do
let(:params) {{ q: "foo" }} let(:params) { { q: "foo" } }
it { is_expected.to be_blank } it { is_expected.to be_blank }
end end
end end
describe 'GET /api/v4/packages/conan/v1/users/authenticate' do describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
it 'responds with 401 Unauthorized when invalid token is provided' do subject { get api('/packages/conan/v1/users/authenticate'), headers: headers }
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', 'wrong-token') }
get api('/packages/conan/v1/users/authenticate'), headers: headers context 'when using invalid token' do
let(:auth_token) { 'invalid_token' }
it 'responds with 401' do
subject
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
end
it 'responds with 200 OK and JWT when valid access token is provided' do context 'when valid JWT access token is provided' do
get api('/packages/conan/v1/users/authenticate'), headers: headers it 'responds with 200' do
subject
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end
it 'token has valid validity time' do
Timecop.freeze do
subject
payload = JSONWebToken::HMACToken.decode(response.body, jwt_secret).first payload = JSONWebToken::HMACToken.decode(
response.body, jwt_secret).first
expect(payload['pat']).to eq(personal_access_token.id) expect(payload['pat']).to eq(personal_access_token.id)
expect(payload['u']).to eq(personal_access_token.user_id) expect(payload['u']).to eq(personal_access_token.user_id)
...@@ -148,6 +155,8 @@ describe API::ConanPackages do ...@@ -148,6 +155,8 @@ describe API::ConanPackages do
expect(duration).to eq(1.hour) expect(duration).to eq(1.hour)
end end
end end
end
end
describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
it 'responds with a 200 OK' do it 'responds with a 200 OK' do
...@@ -163,9 +172,10 @@ describe API::ConanPackages do ...@@ -163,9 +172,10 @@ describe API::ConanPackages do
end end
end end
shared_examples 'rejected invalid recipe' do shared_examples 'rejects invalid recipe' do
context 'with invalid recipe url' do context 'with invalid recipe path' do
let(:recipe) { '../../foo++../..' } let(:recipe_path) { '../../foo++../..' }
it 'returns 400' do it 'returns 400' do
subject subject
...@@ -174,103 +184,295 @@ describe API::ConanPackages do ...@@ -174,103 +184,295 @@ describe API::ConanPackages do
end end
end end
shared_examples 'rejects recipe for invalid project' do
context 'with invalid recipe path' do
let(:recipe_path) { 'aa/bb/not-existing-project/ccc' }
it 'returns forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
shared_examples 'rejects recipe for not found package' do
context 'with invalid recipe path' do
let(:recipe_path) do
'aa/bb/%{project}/ccc' % { project: ::Packages::ConanMetadatum.package_username_from(full_path: project.full_path) }
end
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
shared_examples 'empty recipe for not found package' do
context 'with invalid recipe url' do
let(:recipe_path) do
'aa/bb/%{project}/ccc' % { project: ::Packages::ConanMetadatum.package_username_from(full_path: project.full_path) }
end
it 'returns not found' do
allow(ConanPackagePresenter).to receive(:new)
.with(
'aa/bb@%{project}/ccc' % { project: ::Packages::ConanMetadatum.package_username_from(full_path: project.full_path) },
user,
project
).and_return(presenter)
allow(presenter).to receive(:recipe_snapshot) { {} }
allow(presenter).to receive(:package_snapshot) { {} }
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq("{}")
end
end
end
context 'recipe endpoints' do context 'recipe endpoints' do
let(:jwt) { build_jwt(personal_access_token) } let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) } let(:headers) { build_auth_headers(jwt.encoded) }
let(:recipe) { 'my-package-name/1.0/username/channel' } let(:package_id) { '123456789' }
let(:presenter) { double('ConanPackagePresenter') }
before do
allow(ConanPackagePresenter).to receive(:new)
.with(package.conan_recipe, user, package.project)
.and_return(presenter)
end
describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
let(:recipe_path) { package.conan_recipe_path }
subject { get api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers }
describe 'GET /api/v4/packages/conan/v1/conans/*recipe' do it_behaves_like 'rejects invalid recipe'
subject { get api("/packages/conan/v1/conans/#{recipe}"), headers: headers } it_behaves_like 'rejects recipe for invalid project'
it_behaves_like 'empty recipe for not found package'
it_behaves_like 'rejected invalid recipe' context 'with existing package' do
it 'returns a hash of files with their md5 hashes' do
expected_response = {
'conanfile.py' => 'md5hash1',
'conanmanifest.txt' => 'md5hash2'
}
allow(presenter).to receive(:recipe_snapshot) { expected_response }
it 'responds with an empty response' do
subject subject
expect(response.body).to be {} expect(json_response).to eq(expected_response)
end
end end
end end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/digest' do describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:package_id' do
subject { get api("/packages/conan/v1/conans/#{recipe}/digest"), headers: headers } let(:recipe_path) { package.conan_recipe_path }
subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{package_id}"), headers: headers }
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects recipe for invalid project'
it_behaves_like 'empty recipe for not found package'
it_behaves_like 'rejected invalid recipe' context 'with existing package' do
it 'returns a hash of md5 values for the files' do
expected_response = {
'conaninfo.txt' => "md5hash1",
'conanmanifest.txt' => "md5hash2",
'conan_package.tgz' => "md5hash3"
}
allow(presenter).to receive(:package_snapshot) { expected_response }
it 'responds with a 404' do
subject subject
expect(response).to have_gitlab_http_status(404) expect(json_response).to eq(expected_response)
end end
end end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/upload_urls' do
let(:params) do
{ "conanfile.py": 24,
"conanmanifext.txt": 123 }
end end
subject { post api("/packages/conan/v1/conans/#{recipe}/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejected invalid recipe' describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers }
it 'returns a set of upload urls for the files requested' do it_behaves_like 'rejects invalid recipe'
subject it_behaves_like 'rejects recipe for invalid project'
context 'with existing package' do
let(:recipe_path) { package.conan_recipe_path }
it 'returns the download urls for each package file' do
expected_response = { expected_response = {
'conanfile.py': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/export/conanfile.py", 'conanfile.py' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/export/conanmanifest.txt" 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
} }
expect(response.body).to eq expected_response.to_json allow(presenter).to receive(:recipe_urls) { expected_response }
subject
expect(json_response).to eq(expected_response)
end end
end end
end
describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:package_id/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{package_id}/digest"), headers: headers }
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects recipe for invalid project'
context 'with existing package' do
let(:recipe_path) { package.conan_recipe_path }
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id' do it 'returns the download urls for the files' do
subject { get api("/packages/conan/v1/conans/#{recipe}/packages/123456789"), headers: headers } expected_response = {
'conaninfo.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
'conan_package.tgz' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
}
it_behaves_like 'rejected invalid recipe' allow(presenter).to receive(:package_urls) { expected_response }
it 'responds with an empty response' do
subject subject
expect(response.body).to be {} expect(json_response).to eq(expected_response)
end
end end
end end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id/digest' do describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
subject { get api("/packages/conan/v1/conans/#{recipe}/packages/123456789/digest"), headers: headers } let(:recipe_path) { package.conan_recipe_path }
let(:params) do
{ "conanfile.py": 24,
"conanmanifext.txt": 123 }
end
subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejected invalid recipe' it_behaves_like 'rejects invalid recipe'
it 'responds with a 404' do it 'returns a set of upload urls for the files requested' do
subject subject
expect(response).to have_gitlab_http_status(404) expected_response = {
'conanfile.py': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
}
expect(response.body).to eq(expected_response.to_json)
end end
end end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id/upload_urls' do describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:package_id/upload_urls' do
let(:recipe_path) { package.conan_recipe_path }
let(:params) do let(:params) do
{ "conaninfo.txt": 24, { "conaninfo.txt": 24,
"conanmanifext.txt": 123, "conanmanifext.txt": 123,
"conan_package.tgz": 523 } "conan_package.tgz": 523 }
end end
context 'valid recipe' do subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params, headers: headers }
subject { post api("/packages/conan/v1/conans/#{recipe}/packages/123456789/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejected invalid recipe' it_behaves_like 'rejects invalid recipe'
it 'returns a set of upload urls for the files requested' do it 'returns a set of upload urls for the files requested' do
expected_response = { expected_response = {
'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conaninfo.txt", 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conanmanifest.txt", 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conan_package.tgz" 'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
} }
subject subject
expect(response.body).to eq expected_response.to_json expect(response.body).to eq(expected_response.to_json)
end
end
end end
context 'file endpoints' do
let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) }
let(:package_file_tgz) { package.package_files.find_by(file_type: 'tgz') }
let(:metadata) { package_file_tgz.conan_file_metadatum }
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
:recipe_revision/export/:file_name' do
let(:recipe_path) { package.conan_recipe_path }
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{file_name}"),
headers: headers
end end
context 'invalid file' do
let(:file_name) { 'badfile.txt' }
it 'returns 404 not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'valid file' do
let(:file_name) { package_file_tgz.file_name }
it 'returns forbidden' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
let(:recipe_path) { package.conan_recipe_path }
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/" \
"#{metadata.conan_package_reference}/#{metadata.package_revision}/#{file_name}"),
headers: headers
end
context 'invalid file' do
let(:file_name) { 'badfile.txt' }
it 'returns 404 not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'valid file' do
let(:file_name) { package_file_tgz.file_name }
it 'returns forbidden' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
end end
...@@ -10,6 +10,10 @@ describe Packages::Conan::SearchService do ...@@ -10,6 +10,10 @@ describe Packages::Conan::SearchService do
subject { described_class.new(user, query: query) } subject { described_class.new(user, query: query) }
before do
project.add_developer(user)
end
describe '#execute' do describe '#execute' do
context 'feature unavailable' do context 'feature unavailable' do
let(:query) { '' } let(:query) { '' }
...@@ -34,7 +38,7 @@ describe Packages::Conan::SearchService do ...@@ -34,7 +38,7 @@ describe Packages::Conan::SearchService do
result = subject.execute result = subject.execute
expect(result.status).to eq :success expect(result.status).to eq :success
expect(result.payload).to eq(results: [conan_package.name, conan_package2.name]) expect(result.payload).to eq(results: [conan_package.conan_recipe, conan_package2.conan_recipe])
end end
end end
...@@ -50,24 +54,24 @@ describe Packages::Conan::SearchService do ...@@ -50,24 +54,24 @@ describe Packages::Conan::SearchService do
end end
context 'with no wildcard' do context 'with no wildcard' do
let(:query) { conan_package.name.split('/').first } let(:query) { conan_package.name }
it 'makes a search using the beginning of the recipe' do it 'makes a search using the beginning of the recipe' do
result = subject.execute result = subject.execute
expect(result.status).to eq :success expect(result.status).to eq :success
expect(result.payload).to eq(results: [conan_package.name]) expect(result.payload).to eq(results: [conan_package.conan_recipe])
end end
end end
context 'with full recipe match' do context 'with full recipe match' do
let(:query) { conan_package.name } let(:query) { conan_package.conan_recipe }
it 'makes an exact search' do it 'makes an exact search' do
result = subject.execute result = subject.execute
expect(result.status).to eq :success expect(result.status).to eq :success
expect(result.payload).to eq(results: [conan_package.name]) expect(result.payload).to eq(results: [conan_package.conan_recipe])
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