diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index c0e9289309a..dc9b1fd86c2 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -847,8 +847,6 @@ table.code { .commit-stat-summary { @include media-breakpoint-up(sm) { - margin-left: -$gl-padding; - padding-left: $gl-padding; background-color: $white; } } diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 9d6c0a003c4..8e9321b04e8 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -10,7 +10,7 @@ class Import::BulkImportsController < ApplicationController POLLING_INTERVAL = 3_000 - rescue_from BulkImports::Clients::HTTP::ConnectionError, with: :bulk_import_connection_error + rescue_from BulkImports::Error, with: :bulk_import_connection_error def configure session[access_token_key] = configure_params[access_token_key]&.strip diff --git a/app/finders/packages/helm/package_files_finder.rb b/app/finders/packages/helm/package_files_finder.rb index 74f9eaaca82..ba400b27554 100644 --- a/app/finders/packages/helm/package_files_finder.rb +++ b/app/finders/packages/helm/package_files_finder.rb @@ -3,6 +3,9 @@ module Packages module Helm class PackageFilesFinder + DEFAULT_PACKAGE_FILES_COUNT = 20 + MAX_PACKAGE_FILES_COUNT = 1000 + def initialize(project, channel, params = {}) @project = project @channel = channel @@ -10,12 +13,18 @@ module Packages end def execute - package_files = Packages::PackageFile.for_helm_with_channel(@project, @channel).preload_helm_file_metadata + package_files = Packages::PackageFile.for_helm_with_channel(@project, @channel) + .limit_recent(limit) by_file_name(package_files) end private + def limit + limit_param = @params[:limit] || DEFAULT_PACKAGE_FILES_COUNT + [limit_param, MAX_PACKAGE_FILES_COUNT].min + end + def by_file_name(files) return files unless @params[:file_name] diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index 5d646313423..04e660b418e 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -4,6 +4,8 @@ # projects to a GitLab instance. It associates the import with the responsible # user. class BulkImport < ApplicationRecord + MINIMUM_GITLAB_MAJOR_VERSION = 14 + belongs_to :user, optional: false has_one :configuration, class_name: 'BulkImports::Configuration' diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index 3ef30c035e8..a1e95f6940c 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -27,6 +27,7 @@ class Packages::PackageFile < ApplicationRecord validates :file_name, uniqueness: { scope: :package }, if: -> { package&.pypi? } scope :recent, -> { order(id: :desc) } + scope :limit_recent, ->(limit) { recent.limit(limit) } scope :for_package_ids, ->(ids) { where(package_id: ids) } scope :with_file_name, ->(file_name) { where(file_name: file_name) } scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) } diff --git a/app/presenters/packages/helm/index_presenter.rb b/app/presenters/packages/helm/index_presenter.rb new file mode 100644 index 00000000000..b64f7198ea8 --- /dev/null +++ b/app/presenters/packages/helm/index_presenter.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Packages + module Helm + class IndexPresenter + include API::Helpers::RelatedResourcesHelpers + + API_VERSION = 'v1' + CHANNEL = 'channel' + INDEX_YAML_SUFFIX = "/#{CHANNEL}/index.yaml" + + def initialize(project, project_id_param, package_files) + @project = project + @project_id_param = project_id_param + @package_files = package_files + end + + def api_version + API_VERSION + end + + def entries + files = @package_files.preload_helm_file_metadata + result = Hash.new { |h, k| h[k] = [] } + + files.find_each do |package_file| + name = package_file.helm_metadata['name'] + result[name] << package_file.helm_metadata.merge({ + 'created' => package_file.created_at.utc.strftime('%Y-%m-%dT%H:%M:%S.%NZ'), + 'digest' => package_file.file_sha256, + 'urls' => ["charts/#{package_file.file_name}"] + }) + end + + result + end + + def generated + Time.zone.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%NZ') + end + + def server_info + path = api_v4_projects_packages_helm_index_yaml_path( + id: ERB::Util.url_encode(@project_id_param), + channel: CHANNEL + ) + { + 'contextPath' => expose_url(path.delete_suffix(INDEX_YAML_SUFFIX)) + } + end + end + end +end diff --git a/lib/api/entities/helm/index.rb b/lib/api/entities/helm/index.rb new file mode 100644 index 00000000000..168298f24b6 --- /dev/null +++ b/lib/api/entities/helm/index.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Helm + class Index < Grape::Entity + expose :api_version, as: :apiVersion + expose :entries + expose :generated + expose :server_info, as: :serverInfo + end + end + end +end diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb index d9010dfd329..e396c9608cf 100644 --- a/lib/api/group_packages.rb +++ b/lib/api/group_packages.rb @@ -43,7 +43,7 @@ module API declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless, :status) ).execute - present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true, namespace: user_group.root_ancestor + present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true, namespace: user_group end end end diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb index dc5630a1395..b8215047e18 100644 --- a/lib/api/helm_packages.rb +++ b/lib/api/helm_packages.rb @@ -15,6 +15,9 @@ module API }.freeze content_type :binary, 'application/octet-stream' + content_type :yaml, 'text/yaml' + + formatter :yaml, -> (object, _) { object.serializable_hash.stringify_keys.to_yaml } authenticate_with do |accept| accept.token_types(:personal_access_token, :deploy_token, :job_token) @@ -34,6 +37,28 @@ module API end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do namespace ':id/packages/helm' do + desc 'Download a chart index' do + detail 'This feature was introduced in GitLab 14.0' + end + params do + requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex + end + + get ":channel/index.yaml" do + authorize_read_package!(authorized_user_project) + + package_files = Packages::Helm::PackageFilesFinder.new( + authorized_user_project, + params[:channel], + order_by: 'created_at', + sort: 'desc' + ).execute + + env['api.format'] = :yaml + present ::Packages::Helm::IndexPresenter.new(authorized_user_project, params[:id], package_files), + with: ::API::Entities::Helm::Index + end + desc 'Download a chart' do detail 'This feature was introduced in GitLab 14.0' end diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index c3918f71427..54c0a0628a7 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -41,7 +41,7 @@ module API declared_params.slice(:order_by, :sort, :package_type, :package_name, :include_versionless, :status) ).execute - present paginate(packages), with: ::API::Entities::Package, user: current_user, namespace: user_project.root_ancestor + present paginate(packages), with: ::API::Entities::Package, user: current_user, namespace: user_project.namespace end desc 'Get a single project package' do @@ -55,7 +55,7 @@ module API package = ::Packages::PackageFinder .new(user_project, params[:package_id]).execute - present package, with: ::API::Entities::Package, user: current_user, namespace: user_project.root_ancestor + present package, with: ::API::Entities::Package, user: current_user, namespace: user_project.namespace end desc 'Remove a package' do diff --git a/lib/bulk_imports/clients/graphql.rb b/lib/bulk_imports/clients/graphql.rb index ca549c4be14..0adc2b1c57f 100644 --- a/lib/bulk_imports/clients/graphql.rb +++ b/lib/bulk_imports/clients/graphql.rb @@ -23,15 +23,19 @@ module BulkImports attr_reader :client - delegate :query, :parse, :execute, to: :client + delegate :query, :parse, to: :client def initialize(url: Gitlab::Saas.com_url, token: nil) @url = Gitlab::Utils.append_path(url, '/api/graphql') @token = token - @client = Graphlient::Client.new( - @url, - options(http: HTTP) - ) + @client = Graphlient::Client.new(@url, options(http: HTTP)) + @compatible_instance_version = false + end + + def execute(*args) + validate_instance_version! + + client.execute(*args) end def options(extra = {}) @@ -44,6 +48,19 @@ module BulkImports } }.merge(extra) end + + def validate_instance_version! + return if @compatible_instance_version + + response = client.execute('{ metadata { version } }') + version = Gitlab::VersionInfo.parse(response.data.metadata.version) + + if version.major < BulkImport::MINIMUM_GITLAB_MAJOR_VERSION + raise ::BulkImports::Error.unsupported_gitlab_version + else + @compatible_instance_version = true + end + end end end end diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb index c5f12d8c2ba..71fa1c2f8dd 100644 --- a/lib/bulk_imports/clients/http.rb +++ b/lib/bulk_imports/clients/http.rb @@ -7,14 +7,13 @@ module BulkImports DEFAULT_PAGE = 1 DEFAULT_PER_PAGE = 30 - ConnectionError = Class.new(StandardError) - def initialize(uri:, token:, page: DEFAULT_PAGE, per_page: DEFAULT_PER_PAGE, api_version: API_VERSION) @uri = URI.parse(uri) @token = token&.strip @page = page @per_page = per_page @api_version = api_version + @compatible_instance_version = false end def get(resource, query = {}) @@ -53,10 +52,28 @@ module BulkImports Gitlab::Utils.append_path(api_url, resource) end + def validate_instance_version! + return if @compatible_instance_version + + response = with_error_handling do + Gitlab::HTTP.get(resource_url(:version), default_options) + end + + version = Gitlab::VersionInfo.parse(response.parsed_response['version']) + + if version.major < BulkImport::MINIMUM_GITLAB_MAJOR_VERSION + raise ::BulkImports::Error.unsupported_gitlab_version + else + @compatible_instance_version = true + end + end + private # rubocop:disable GitlabSecurity/PublicSend def request(method, resource, options = {}, &block) + validate_instance_version! + with_error_handling do Gitlab::HTTP.public_send( method, @@ -96,11 +113,11 @@ module BulkImports def with_error_handling response = yield - raise ConnectionError, "Error #{response.code}" unless response.success? + raise(::BulkImports::Error, "Error #{response.code}") unless response.success? response rescue *Gitlab::HTTP::HTTP_ERRORS => e - raise ConnectionError, e + raise(::BulkImports::Error, e) end def base_uri diff --git a/lib/bulk_imports/error.rb b/lib/bulk_imports/error.rb new file mode 100644 index 00000000000..0464aea642e --- /dev/null +++ b/lib/bulk_imports/error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module BulkImports + class Error < StandardError + def self.unsupported_gitlab_version + self.new("Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.") + end + end +end diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb index 8f74d210667..4385342e1c6 100644 --- a/spec/controllers/import/bulk_imports_controller_spec.rb +++ b/spec/controllers/import/bulk_imports_controller_spec.rb @@ -149,7 +149,7 @@ RSpec.describe Import::BulkImportsController do context 'when connection error occurs' do before do allow(controller).to receive(:client).and_return(client) - allow(client).to receive(:get).and_raise(BulkImports::Clients::HTTP::ConnectionError) + allow(client).to receive(:get).and_raise(BulkImports::Error) end it 'returns 422' do diff --git a/spec/factories/packages/helm/file_metadatum.rb b/spec/factories/packages/helm/file_metadatum.rb index e809f592546..cbc7e114ef6 100644 --- a/spec/factories/packages/helm/file_metadatum.rb +++ b/spec/factories/packages/helm/file_metadatum.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :helm_file_metadatum, class: 'Packages::Helm::FileMetadatum' do package_file { association(:helm_package_file, without_loaded_metadatum: true) } - channel { 'stable' } + sequence(:channel) { |n| "#{FFaker::Lorem.word}-#{n}" } metadata { { 'name': package_file.package.name, 'version': package_file.package.version, 'apiVersion': 'v2' } } end end diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb index 9b60f769505..ac121da432c 100644 --- a/spec/factories/packages/package_file.rb +++ b/spec/factories/packages/package_file.rb @@ -205,6 +205,7 @@ FactoryBot.define do package { association(:helm_package, without_package_files: true) } file_name { "#{package.name}-#{package.version}.tgz" } file_fixture { "spec/fixtures/packages/helm/rook-ceph-v1.5.8.tgz" } + file_sha256 { 'fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db' } transient do without_loaded_metadatum { false } diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb index 563c8f429f8..4ed2137f7ae 100644 --- a/spec/features/groups/import_export/connect_instance_spec.rb +++ b/spec/features/groups/import_export/connect_instance_spec.rb @@ -24,6 +24,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do pat = 'demo-pat' stub_path = 'stub-group' total = 37 + stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=50&search=" % { url: source_url }).to_return( body: [{ id: 2595438, @@ -32,7 +33,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do path: stub_path, full_name: 'Stub', full_path: stub_path - }].to_json, + }].to_json, headers: { 'Content-Type' => 'application/json', 'X-Next-Page' => 2, @@ -43,6 +44,10 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do } ) + allow_next_instance_of(BulkImports::Clients::HTTP) do |client| + allow(client).to receive(:validate_instance_version!).and_return(true) + end + expect(page).to have_content 'Import groups from another instance of GitLab' expect(page).to have_content 'Not all related objects are migrated' @@ -53,6 +58,8 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do expect(page).to have_content 'Showing 1-1 of %{total} groups from %{url}' % { url: source_url, total: total } expect(page).to have_content stub_path + + wait_for_all_requests end end diff --git a/spec/lib/api/entities/package_spec.rb b/spec/lib/api/entities/package_spec.rb new file mode 100644 index 00000000000..d63ea7833ac --- /dev/null +++ b/spec/lib/api/entities/package_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Package do + let(:package) { create(:generic_package) } + + subject { described_class.new(package).as_json(namespace: package.project.namespace) } + + it 'exposes correct attributes' do + expect(subject).to include( + :id, + :name, + :version, + :package_type, + :status, + :_links, + :created_at, + :tags, + :versions + ) + end + + it 'exposes correct web_path in _links' do + expect(subject[:_links][:web_path]).to match('/packages/') + end + + context 'with a terraform_module' do + let(:package) { create(:terraform_module_package) } + + it 'exposes correct web_path in _links' do + expect(subject[:_links][:web_path]).to match('/infrastructure_registry/') + end + end +end diff --git a/spec/lib/bulk_imports/clients/graphql_spec.rb b/spec/lib/bulk_imports/clients/graphql_spec.rb new file mode 100644 index 00000000000..2f212458c4a --- /dev/null +++ b/spec/lib/bulk_imports/clients/graphql_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Clients::Graphql do + let_it_be(:config) { create(:bulk_import_configuration) } + + subject { described_class.new(url: config.url, token: config.access_token) } + + describe '#execute' do + let(:query) { '{ metadata { version } }' } + let(:graphql_client_double) { double } + let(:response_double) { double } + + before do + stub_const('BulkImports::MINIMUM_COMPATIBLE_MAJOR_VERSION', version) + allow(graphql_client_double).to receive(:execute) + allow(subject).to receive(:client).and_return(graphql_client_double) + allow(graphql_client_double).to receive(:execute).with(query).and_return(response_double) + allow(response_double).to receive_message_chain(:data, :metadata, :version).and_return(version) + end + + context 'when source instance is compatible' do + let(:version) { '14.0.0' } + + it 'marks source instance as compatible' do + subject.execute('test') + + expect(subject.instance_variable_get(:@compatible_instance_version)).to eq(true) + end + end + + context 'when source instance is incompatible' do + let(:version) { '13.0.0' } + + it 'raises an error' do + expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.") + end + end + end +end diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb index ac42f12a3d4..d3a289da467 100644 --- a/spec/lib/bulk_imports/clients/http_spec.rb +++ b/spec/lib/bulk_imports/clients/http_spec.rb @@ -8,7 +8,15 @@ RSpec.describe BulkImports::Clients::HTTP do let(:uri) { 'http://gitlab.example' } let(:token) { 'token' } let(:resource) { 'resource' } + let(:version) { "#{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.0.0" } let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } + let(:version_response) { double(code: 200, success?: true, parsed_response: { 'version' => version }) } + + before do + allow(Gitlab::HTTP).to receive(:get) + .with('http://gitlab.example:80/api/v4/version', anything) + .and_return(version_response) + end subject { described_class.new(uri: uri, token: token) } @@ -21,20 +29,20 @@ RSpec.describe BulkImports::Clients::HTTP do context 'error handling' do context 'when error occurred' do - it 'raises ConnectionError' do + it 'raises BulkImports::Error' do allow(Gitlab::HTTP).to receive(method).and_raise(Errno::ECONNREFUSED) - expect { subject.public_send(method, resource) }.to raise_exception(described_class::ConnectionError) + expect { subject.public_send(method, resource) }.to raise_exception(BulkImports::Error) end end context 'when response is not success' do - it 'raises ConnectionError' do + it 'raises BulkImports::Error' do response_double = double(code: 503, success?: false) allow(Gitlab::HTTP).to receive(method).and_return(response_double) - expect { subject.public_send(method, resource) }.to raise_exception(described_class::ConnectionError) + expect { subject.public_send(method, resource) }.to raise_exception(BulkImports::Error) end end end @@ -167,4 +175,12 @@ RSpec.describe BulkImports::Clients::HTTP do subject.stream(resource) end end + + context 'when source instance is incompatible' do + let(:version) { '13.0.0' } + + it 'raises an error' do + expect { subject.get(resource) }.to raise_error(::BulkImports::Error, "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.") + end + end end diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb index 5871c0a5d5b..08edacb329f 100644 --- a/spec/requests/api/helm_packages_spec.rb +++ b/spec/requests/api/helm_packages_spec.rb @@ -9,13 +9,16 @@ RSpec.describe API::HelmPackages do let_it_be_with_reload(:project) { create(:project, :public) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } + let_it_be(:package) { create(:helm_package, project: project) } + + describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do + it_behaves_like 'handling helm chart index requests' do + let(:url) { "/projects/#{project.id}/packages/helm/#{package.package_files.first.helm_channel}/index.yaml" } + end + end describe 'GET /api/v4/projects/:id/packages/helm/:channel/charts/:file_name.tgz' do - let_it_be(:package) { create(:helm_package, project: project) } - - let(:channel) { package.package_files.first.helm_channel } - - let(:url) { "/projects/#{project.id}/packages/helm/#{channel}/charts/#{package.name}-#{package.version}.tgz" } + let(:url) { "/projects/#{project.id}/packages/helm/#{package.package_files.first.helm_channel}/charts/#{package.name}-#{package.version}.tgz" } subject { get api(url) } diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 5886f293f41..9b7538547f6 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' RSpec.describe API::ProjectPackages do - let(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } + + let(:user) { create(:user) } let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" } let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') } @@ -71,6 +72,20 @@ RSpec.describe API::ProjectPackages do expect(json_response).to include(a_hash_including('_links' => a_hash_including('web_path' => include('infrastructure_registry')))) end end + + context 'in nested group' do + let_it_be(:nested_project) { create(:project, :public, :in_subgroup) } + let_it_be(:nested_terraform_module_package) { create(:terraform_module_package, project: nested_project) } + + let(:params) { { package_type: :terraform_module } } + let(:url) { "/projects/#{nested_project.id}/packages" } + + it 'returns the nested terraform module package with the correct web_path' do + subject + + expect(json_response).to include(a_hash_including('_links' => a_hash_including('web_path' => include(nested_project.namespace.full_path)))) + end + end end context 'project is private' do diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 585c4fb8a4e..8a173ff99da 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -18,6 +18,37 @@ RSpec.shared_examples 'rejects helm packages access' do |user_type, status, add_ end end +RSpec.shared_examples 'process helm service index request' 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 'returns a valid YAML response', :aggregate_failures do + subject + + expect(response.media_type).to eq('text/yaml') + expect(response.body).to start_with("---\napiVersion: v1\nentries:\n") + + yaml_response = YAML.safe_load(response.body) + + expect(yaml_response.keys).to contain_exactly('apiVersion', 'entries', 'generated', 'serverInfo') + expect(yaml_response['entries']).to be_a(Hash) + expect(yaml_response['entries'].keys).to contain_exactly(package.name) + expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "http://localhost/api/v4/projects/#{project.id}/packages/helm" }) + + package_entry = yaml_response['entries'][package.name] + + expect(package_entry.length).to eq(1) + expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls') + expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db') + expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"]) + end + end +end + RSpec.shared_examples 'process helm download content request' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do @@ -51,3 +82,89 @@ RSpec.shared_examples 'rejects helm access with unknown project id' do end end end + +RSpec.shared_examples 'handling helm chart index requests' do |anonymous_requests_example_name: 'process helm service index request', anonymous_requests_status: :success| + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + context 'personal token' do + where(:visibility, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + :public | :developer | true | true | 'process helm service index request' | :success + :public | :guest | true | true | 'process helm service index request' | :success + :public | :developer | true | false | 'rejects helm packages access' | :unauthorized + :public | :guest | true | false | 'rejects helm packages access' | :unauthorized + :public | :developer | false | true | 'process helm service index request' | :success + :public | :guest | false | true | 'process helm service index request' | :success + :public | :developer | false | false | 'rejects helm packages access' | :unauthorized + :public | :guest | false | false | 'rejects helm packages access' | :unauthorized + :public | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status + :private | :developer | true | true | 'process helm service index request' | :success + :private | :guest | true | true | 'rejects helm packages access' | :forbidden + :private | :developer | true | false | 'rejects helm packages access' | :unauthorized + :private | :guest | true | false | 'rejects helm packages access' | :unauthorized + :private | :developer | false | true | 'rejects helm packages access' | :not_found + :private | :guest | false | true | 'rejects helm packages access' | :not_found + :private | :developer | false | false | 'rejects helm packages access' | :unauthorized + :private | :guest | false | false | 'rejects helm packages access' | :unauthorized + :private | :anonymous | false | true | 'rejects helm packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + context 'with job token' do + where(:visibility, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + :public | :developer | true | true | 'process helm service index request' | :success + :public | :guest | true | true | 'process helm service index request' | :success + :public | :developer | true | false | 'rejects helm packages access' | :unauthorized + :public | :guest | true | false | 'rejects helm packages access' | :unauthorized + :public | :developer | false | true | 'process helm service index request' | :success + :public | :guest | false | true | 'process helm service index request' | :success + :public | :developer | false | false | 'rejects helm packages access' | :unauthorized + :public | :guest | false | false | 'rejects helm packages access' | :unauthorized + :public | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status + :private | :developer | true | true | 'process helm service index request' | :success + :private | :guest | true | true | 'rejects helm packages access' | :forbidden + :private | :developer | true | false | 'rejects helm packages access' | :unauthorized + :private | :guest | true | false | 'rejects helm packages access' | :unauthorized + :private | :developer | false | true | 'rejects helm packages access' | :not_found + :private | :guest | false | true | 'rejects helm packages access' | :not_found + :private | :developer | false | false | 'rejects helm packages access' | :unauthorized + :private | :guest | false | false | 'rejects helm packages access' | :unauthorized + :private | :anonymous | false | true | 'rejects helm packages access' | :unauthorized + end + + with_them do + let_it_be(:ci_build) { create(:ci_build, project: project, user: user, status: :running) } + + let(:job) { user_token ? ci_build : double(token: 'wrong') } + let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects helm access with unknown project id' do + subject { get api(url) } + end +end diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb index 8d528011752..9eabf7b8f0f 100644 --- a/spec/workers/bulk_imports/export_request_worker_spec.rb +++ b/spec/workers/bulk_imports/export_request_worker_spec.rb @@ -6,12 +6,17 @@ RSpec.describe BulkImports::ExportRequestWorker do let_it_be(:bulk_import) { create(:bulk_import) } let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) } let_it_be(:entity) { create(:bulk_import_entity, source_full_path: 'foo/bar', bulk_import: bulk_import) } + let_it_be(:version_url) { 'https://gitlab.example:443/api/v4/version' } let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } let(:job_args) { [entity.id] } describe '#perform' do before do + allow(Gitlab::HTTP) + .to receive(:get) + .with(version_url, anything) + .and_return(double(code: 200, success?: true, parsed_response: { 'version' => Gitlab::VERSION })) allow(Gitlab::HTTP).to receive(:post).and_return(response_double) end