From db35842f7d18e38d57c8273ff94ca9aa4873b6dd Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Sun, 22 Mar 2020 02:12:53 -0500 Subject: [PATCH 01/19] Implement Go modules proxy --- ee/lib/api/go_proxy.rb | 122 +++++++++++++++++++++++++++++++++++++++++ ee/lib/ee/api/api.rb | 1 + 2 files changed, 123 insertions(+) create mode 100755 ee/lib/api/go_proxy.rb diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb new file mode 100755 index 00000000000000..e2d68f9e86d5ee --- /dev/null +++ b/ee/lib/api/go_proxy.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true +module API + class GoProxy < Grape::API + helpers ::API::Helpers::PackagesHelpers + + SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[-.A-Z0-9]+)?(\+[-.A-Z0-9]+)?/i.freeze + SEMVER_TAG_REGEX = Regexp.new("^#{SEMVER_REGEX.source}$").freeze + MODULE_VERSION_REQUIREMENTS = { :module_version => SEMVER_REGEX } + + helpers do + def project_package_base + @project_package_base ||= Gitlab::Routing.url_helpers.project_url(user_project).split('://', 2)[1] + end + + def check_module_name + module_name = params[:module_name].gsub(/![[:alpha:]]/) { |s| s[1..].upcase } + + bad_request!('Module Name') if module_name.blank? + + if module_name == project_package_base + [module_name, ''] + elsif module_name.start_with?(project_package_base + '/') + [module_name, module_name[(project_package_base.length+1)..]] + else + not_found! + end + end + + def module_version?(project, path, module_name, tag) + return false unless SEMVER_TAG_REGEX.match?(tag.name) + return false unless tag.dereferenced_target + + gomod = project.repository.blob_at(tag.dereferenced_target.sha, path + '/go.mod') + return false unless gomod + + mod = gomod.data.split("\n", 2).first + mod == 'module ' + module_name + end + + def module_versions(project, path, module_name) + project.repository.tags.filter { |tag| module_version?(project, path, module_name, tag) } + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/packages/go/*module_name/@v' do + before do + end + + desc 'Get all tagged versions for a given Go module' do + detail 'See `go help goproxy`, GET $GOPROXY//@v/list' + end + get 'list' do + module_name, path = check_module_name + + content_type 'text/plain' + module_versions(user_project, path, module_name).map { |t| t.name }.join("\n") + end + + desc 'Get information about the given module version' do + detail 'See `go help goproxy`, GET $GOPROXY//@v/.info' + end + get ':module_version.info', :requirements => MODULE_VERSION_REQUIREMENTS do + module_name, path = check_module_name + + tag = user_project.repository.tags.filter { |tag| tag.name == params[:module_version] }.first + not_found! unless tag && module_version?(user_project, path, module_name, tag) + + { + "Version" => tag.name, + "Time" => tag.dereferenced_target.committed_date + } + end + + desc 'Get the module file of the given module version' do + detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod' + end + get ':module_version.mod', :requirements => MODULE_VERSION_REQUIREMENTS do + module_name, path = check_module_name + + tag = user_project.repository.tags.filter { |tag| tag.name == params[:module_version] }.first + not_found! unless tag && module_version?(user_project, path, module_name, tag) + + content_type 'text/plain' + user_project.repository.blob_at(tag.dereferenced_target.sha, path + '/go.mod').data + end + + desc 'Get a zip of the source of the given module version' do + detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip' + end + get ':module_version.zip', :requirements => MODULE_VERSION_REQUIREMENTS do + module_name, path = check_module_name + + tag = user_project.repository.tags.filter { |tag| tag.name == params[:module_version] }.first + not_found! unless tag && module_version?(user_project, path, module_name, tag) + + sha = tag.dereferenced_target.sha + tree = user_project.repository.tree(sha, path, recursive: true).entries.filter { |e| e.type == :blob } + nested = tree.filter { |e| e.name == 'go.mod' && !(path == '' && e.path == 'go.mod' || e.path == path + '/go.mod') }.map { |e| e.path[0..-7] } + files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } + + s = Zip::OutputStream.write_buffer do |zip| + files.each do |file| + zip.put_next_entry(file.path) + zip.write user_project.repository.blob_at(sha, file.path).data + end + end + + header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: tag.name + '.zip') + header['Content-Transfer-Encoding'] = 'binary' + content_type 'text/plain' + # content_type 'application/zip' + status :ok + body s.string + end + end + end + end +end \ No newline at end of file diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb index a3d4a25d23a365..e94ce4d4d789a7 100644 --- a/ee/lib/ee/api/api.rb +++ b/ee/lib/ee/api/api.rb @@ -38,6 +38,7 @@ module API mount ::API::ConanPackages mount ::API::MavenPackages mount ::API::NpmPackages + mount ::API::GoProxy mount ::API::MergeTrains mount ::API::ProjectPackages mount ::API::GroupPackages -- GitLab From d43bff87a55439a81c23fe145e2e2467e4426791 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Sun, 22 Mar 2020 17:39:30 -0500 Subject: [PATCH 02/19] Utilize MVVM for Go module proxy Implement models, entities, presenters --- ee/app/models/packages/go_module.rb | 40 +++++++++ ee/app/models/packages/go_module_version.rb | 67 ++++++++++++++ .../packages/go/module_version_presenter.rb | 19 ++++ ee/lib/api/go_proxy.rb | 90 +++++++------------ ee/lib/ee/api/entities/go_module_version.rb | 12 +++ 5 files changed, 171 insertions(+), 57 deletions(-) create mode 100644 ee/app/models/packages/go_module.rb create mode 100644 ee/app/models/packages/go_module_version.rb create mode 100644 ee/app/presenters/packages/go/module_version_presenter.rb create mode 100644 ee/lib/ee/api/entities/go_module_version.rb diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb new file mode 100644 index 00000000000000..a4da70ce2e5698 --- /dev/null +++ b/ee/app/models/packages/go_module.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Packages::GoModule #< ApplicationRecord + SEMVER_TAG_REGEX = Regexp.new("^#{::Packages::GoModuleVersion::SEMVER_REGEX.source}$").freeze + + # belongs_to :project + + attr_reader :project, :name, :path, :versions + + def initialize(project, name) + @project = project + @name = name + + @path = + if @name == package_base + '' + elsif @name.start_with?(package_base + '/') + @name[(package_base.length+1)..] + else + nil + end + end + + def versions + @versions ||= project.repository.tags. + filter { |tag| SEMVER_TAG_REGEX.match?(tag.name) && !tag.dereferenced_target.nil? }. + map { |tag| ::Packages::GoModuleVersion.new self, tag }. + filter { |ver| ver.valid? } + end + + def find_version(name) + versions.filter { |ver| ver.name == name }.first + end + + private + + def package_base + @package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1] + end +end diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb new file mode 100644 index 00000000000000..06dfc8bf072ade --- /dev/null +++ b/ee/app/models/packages/go_module_version.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class Packages::GoModuleVersion #< ApplicationRecord + SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.A-Z0-9]+))?(?:\+([-.A-Z0-9]+))?/i.freeze + VERSION_SUFFIX_REGEX = /\/v([1-9]\d*)$/i.freeze + + # belongs_to :mod + + attr_reader :mod, :tag + + delegate :name, to: :tag + + def initialize(mod, tag) + @mod = mod + @tag = tag + end + + def gomod + return @gomod unless @gomod.nil? + + blob = @mod.project.repository.blob_at(tag.dereferenced_target.sha, @mod.path + '/go.mod') + @gomod = blob ? blob.data : '' + end + + def valid? + m = gomod.split("\n", 2).first + case major + when 0, 1 + m == "module #{@mod.name}" + else + m == "module #{@mod.name}/v#{major}" + end + end + + def major + SEMVER_REGEX.match(@tag.name)[1].to_i + end + + def minor + SEMVER_REGEX.match(@tag.name)[2].to_i + end + + def patch + SEMVER_REGEX.match(@tag.name)[3].to_i + end + + def prerelease + SEMVER_REGEX.match(@tag.name)[4] + end + + def build + SEMVER_REGEX.match(@tag.name)[5] + end + + def files + return @files unless @files.nil? + + sha = @tag.dereferenced_target.sha + tree = @mod.project.repository.tree(sha, mod.path, recursive: true).entries.filter { |e| e.file? } + nested = tree.filter { |e| e.name == 'go.mod' && !(mod.path == '' && e.path == 'go.mod' || e.path == mod.path + '/go.mod') }.map { |e| e.path[0..-7] } + @files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } + end + + def blob_at(path) + @mod.project.repository.blob_at(tag.dereferenced_target.sha, path).data + end +end diff --git a/ee/app/presenters/packages/go/module_version_presenter.rb b/ee/app/presenters/packages/go/module_version_presenter.rb new file mode 100644 index 00000000000000..f3f5663cbab8db --- /dev/null +++ b/ee/app/presenters/packages/go/module_version_presenter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Packages + module Go + class ModuleVersionPresenter + def initialize(version) + @version = version + end + + def name + @version.name + end + + def time + @version.tag.dereferenced_target.committed_date + end + end + end +end \ No newline at end of file diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index e2d68f9e86d5ee..eb8549cb91d7f2 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -3,113 +3,89 @@ module API class GoProxy < Grape::API helpers ::API::Helpers::PackagesHelpers - SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[-.A-Z0-9]+)?(\+[-.A-Z0-9]+)?/i.freeze - SEMVER_TAG_REGEX = Regexp.new("^#{SEMVER_REGEX.source}$").freeze - MODULE_VERSION_REQUIREMENTS = { :module_version => SEMVER_REGEX } + MODULE_VERSION_REQUIREMENTS = { :module_version => ::Packages::GoModuleVersion::SEMVER_REGEX } helpers do - def project_package_base - @project_package_base ||= Gitlab::Routing.url_helpers.project_url(user_project).split('://', 2)[1] - end - - def check_module_name + def find_module module_name = params[:module_name].gsub(/![[:alpha:]]/) { |s| s[1..].upcase } bad_request!('Module Name') if module_name.blank? - if module_name == project_package_base - [module_name, ''] - elsif module_name.start_with?(project_package_base + '/') - [module_name, module_name[(project_package_base.length+1)..]] - else - not_found! - end - end - - def module_version?(project, path, module_name, tag) - return false unless SEMVER_TAG_REGEX.match?(tag.name) - return false unless tag.dereferenced_target - - gomod = project.repository.blob_at(tag.dereferenced_target.sha, path + '/go.mod') - return false unless gomod - - mod = gomod.data.split("\n", 2).first - mod == 'module ' + module_name - end + mod = ::Packages::GoModule.new user_project, module_name + not_found! if mod.path.nil? - def module_versions(project, path, module_name) - project.repository.tags.filter { |tag| module_version?(project, path, module_name, tag) } + mod end end params do requires :id, type: String, desc: 'The ID of a project' + requires :module_name, type: String, desc: 'Module name' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do namespace ':id/packages/go/*module_name/@v' do - before do - end - desc 'Get all tagged versions for a given Go module' do detail 'See `go help goproxy`, GET $GOPROXY//@v/list' end get 'list' do - module_name, path = check_module_name + mod = find_module content_type 'text/plain' - module_versions(user_project, path, module_name).map { |t| t.name }.join("\n") + mod.versions.map { |t| t.name }.join("\n") end desc 'Get information about the given module version' do detail 'See `go help goproxy`, GET $GOPROXY//@v/.info' + success EE::API::Entities::GoModuleVersion + end + params do + requires :module_version, type: String, desc: 'Module version' end get ':module_version.info', :requirements => MODULE_VERSION_REQUIREMENTS do - module_name, path = check_module_name + mod = find_module - tag = user_project.repository.tags.filter { |tag| tag.name == params[:module_version] }.first - not_found! unless tag && module_version?(user_project, path, module_name, tag) + ver = mod.find_version params[:module_version] + not_found! unless ver - { - "Version" => tag.name, - "Time" => tag.dereferenced_target.committed_date - } + present ::Packages::Go::ModuleVersionPresenter.new(ver), with: EE::API::Entities::GoModuleVersion end desc 'Get the module file of the given module version' do detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod' end + params do + requires :module_version, type: String, desc: 'Module version' + end get ':module_version.mod', :requirements => MODULE_VERSION_REQUIREMENTS do - module_name, path = check_module_name + mod = find_module - tag = user_project.repository.tags.filter { |tag| tag.name == params[:module_version] }.first - not_found! unless tag && module_version?(user_project, path, module_name, tag) + ver = mod.find_version params[:module_version] + not_found! unless ver content_type 'text/plain' - user_project.repository.blob_at(tag.dereferenced_target.sha, path + '/go.mod').data + ver.gomod end desc 'Get a zip of the source of the given module version' do detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip' end + params do + requires :module_version, type: String, desc: 'Module version' + end get ':module_version.zip', :requirements => MODULE_VERSION_REQUIREMENTS do - module_name, path = check_module_name - - tag = user_project.repository.tags.filter { |tag| tag.name == params[:module_version] }.first - not_found! unless tag && module_version?(user_project, path, module_name, tag) + mod = find_module - sha = tag.dereferenced_target.sha - tree = user_project.repository.tree(sha, path, recursive: true).entries.filter { |e| e.type == :blob } - nested = tree.filter { |e| e.name == 'go.mod' && !(path == '' && e.path == 'go.mod' || e.path == path + '/go.mod') }.map { |e| e.path[0..-7] } - files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } + ver = mod.find_version params[:module_version] + not_found! unless ver s = Zip::OutputStream.write_buffer do |zip| - files.each do |file| - zip.put_next_entry(file.path) - zip.write user_project.repository.blob_at(sha, file.path).data + ver.files.each do |file| + zip.put_next_entry file.path + zip.write ver.blob_at(file.path) end end - header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: tag.name + '.zip') + header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: ver.name + '.zip') header['Content-Transfer-Encoding'] = 'binary' content_type 'text/plain' # content_type 'application/zip' diff --git a/ee/lib/ee/api/entities/go_module_version.rb b/ee/lib/ee/api/entities/go_module_version.rb new file mode 100644 index 00000000000000..6c491cb163743f --- /dev/null +++ b/ee/lib/ee/api/entities/go_module_version.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module EE + module API + module Entities + class GoModuleVersion < Grape::Entity + expose :name, as: 'Version' + expose :time, as: 'Time' + end + end + end + end -- GitLab From 35df28bc496d7b0440a170427e18147168a513b2 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Sun, 22 Mar 2020 17:39:41 -0500 Subject: [PATCH 03/19] Work on Go module proxy - Call package authorization helpers - Add change long entry - Fix rubocop violations - Add basic documentation --- doc/administration/packages/index.md | 1 + doc/user/packages/go_proxy/index.md | 24 +++++++++++++++++++ ee/app/models/packages/go_module.rb | 14 +++++------ ee/app/models/packages/go_module_version.rb | 2 +- .../packages/go/module_version_presenter.rb | 2 +- .../unreleased/27376-go-package-mvc.yml | 5 ++++ ee/lib/api/go_proxy.rb | 17 +++++++++---- ee/lib/ee/api/entities/go_module_version.rb | 12 +++++----- 8 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 doc/user/packages/go_proxy/index.md create mode 100644 ee/changelogs/unreleased/27376-go-package-mvc.yml diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index f826741d66f82a..ac9b342d2d8af7 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -13,6 +13,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [Conan Repository](../../user/packages/conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ | | [Maven Repository](../../user/packages/maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | | [NPM Registry](../../user/packages/npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | +| [Go Proxy](../../user/packages/go_proxy/index.md) | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | ??.??+ | Don't you see your package management system supported yet? Please consider contributing diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md new file mode 100644 index 00000000000000..b9e78566c31224 --- /dev/null +++ b/doc/user/packages/go_proxy/index.md @@ -0,0 +1,24 @@ +# GitLab Go Proxy **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab Premium](https://about.gitlab.com/pricing/) ??.??. + +The GitLab Go Proxy implements the Go proxy protocol. + +NOTE: **Note:** +GitLab does not (yet) display Go modules in the **Packages** section of a project. +Only the Go proxy protocol is supported at this time. + +## Enabling the Go proxy + +NOTE: **Note:** +This option is available only if your GitLab administrator has +[enabled support for the Package Registry](../../../administration/packages/index.md). **(PREMIUM ONLY)** + +After the Package Registry is enabled, it will be available for all new projects +by default. To enable it for existing projects, or if you want to disable it: + +1. Navigate to your project's **Settings > General > Permissions**. +1. Find the Packages feature and enable or disable it. +1. Click on **Save changes** for the changes to take effect. + +You should then be able to see the **Packages** section on the left sidebar. diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb index a4da70ce2e5698..2cb7ee9897dc6e 100644 --- a/ee/app/models/packages/go_module.rb +++ b/ee/app/models/packages/go_module.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -class Packages::GoModule #< ApplicationRecord +class Packages::GoModule SEMVER_TAG_REGEX = Regexp.new("^#{::Packages::GoModuleVersion::SEMVER_REGEX.source}$").freeze # belongs_to :project - attr_reader :project, :name, :path, :versions + attr_reader :project, :name, :path def initialize(project, name) @project = project @@ -15,17 +15,17 @@ def initialize(project, name) if @name == package_base '' elsif @name.start_with?(package_base + '/') - @name[(package_base.length+1)..] + @name[(package_base.length + 1)..] else nil end end def versions - @versions ||= project.repository.tags. - filter { |tag| SEMVER_TAG_REGEX.match?(tag.name) && !tag.dereferenced_target.nil? }. - map { |tag| ::Packages::GoModuleVersion.new self, tag }. - filter { |ver| ver.valid? } + @versions ||= project.repository.tags + .filter { |tag| SEMVER_TAG_REGEX.match?(tag.name) && !tag.dereferenced_target.nil? } + .map { |tag| ::Packages::GoModuleVersion.new self, tag } + .filter { |ver| ver.valid? } end def find_version(name) diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index 06dfc8bf072ade..7c3830d479dc57 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Packages::GoModuleVersion #< ApplicationRecord +class Packages::GoModuleVersion SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.A-Z0-9]+))?(?:\+([-.A-Z0-9]+))?/i.freeze VERSION_SUFFIX_REGEX = /\/v([1-9]\d*)$/i.freeze diff --git a/ee/app/presenters/packages/go/module_version_presenter.rb b/ee/app/presenters/packages/go/module_version_presenter.rb index f3f5663cbab8db..9e160406e070d2 100644 --- a/ee/app/presenters/packages/go/module_version_presenter.rb +++ b/ee/app/presenters/packages/go/module_version_presenter.rb @@ -16,4 +16,4 @@ def time end end end -end \ No newline at end of file +end diff --git a/ee/changelogs/unreleased/27376-go-package-mvc.yml b/ee/changelogs/unreleased/27376-go-package-mvc.yml new file mode 100644 index 00000000000000..ad95d57e009536 --- /dev/null +++ b/ee/changelogs/unreleased/27376-go-package-mvc.yml @@ -0,0 +1,5 @@ +--- +title: Implement Go module proxy MVC (package manager for Go) +merge_request: 27746 +author: Ethan Reesor +type: added diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index eb8549cb91d7f2..11c55278b9c8c8 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -3,7 +3,9 @@ module API class GoProxy < Grape::API helpers ::API::Helpers::PackagesHelpers - MODULE_VERSION_REQUIREMENTS = { :module_version => ::Packages::GoModuleVersion::SEMVER_REGEX } + MODULE_VERSION_REQUIREMENTS = { module_version: ::Packages::GoModuleVersion::SEMVER_REGEX }.freeze + + before { require_packages_enabled! } helpers do def find_module @@ -23,6 +25,11 @@ def find_module requires :module_name, type: String, desc: 'Module name' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + before do + authorize_read_package! + authorize_packages_feature! + end + namespace ':id/packages/go/*module_name/@v' do desc 'Get all tagged versions for a given Go module' do detail 'See `go help goproxy`, GET $GOPROXY//@v/list' @@ -41,7 +48,7 @@ def find_module params do requires :module_version, type: String, desc: 'Module version' end - get ':module_version.info', :requirements => MODULE_VERSION_REQUIREMENTS do + get ':module_version.info', requirements: MODULE_VERSION_REQUIREMENTS do mod = find_module ver = mod.find_version params[:module_version] @@ -56,7 +63,7 @@ def find_module params do requires :module_version, type: String, desc: 'Module version' end - get ':module_version.mod', :requirements => MODULE_VERSION_REQUIREMENTS do + get ':module_version.mod', requirements: MODULE_VERSION_REQUIREMENTS do mod = find_module ver = mod.find_version params[:module_version] @@ -72,7 +79,7 @@ def find_module params do requires :module_version, type: String, desc: 'Module version' end - get ':module_version.zip', :requirements => MODULE_VERSION_REQUIREMENTS do + get ':module_version.zip', requirements: MODULE_VERSION_REQUIREMENTS do mod = find_module ver = mod.find_version params[:module_version] @@ -95,4 +102,4 @@ def find_module end end end -end \ No newline at end of file +end diff --git a/ee/lib/ee/api/entities/go_module_version.rb b/ee/lib/ee/api/entities/go_module_version.rb index 6c491cb163743f..a7d106fbe13b65 100644 --- a/ee/lib/ee/api/entities/go_module_version.rb +++ b/ee/lib/ee/api/entities/go_module_version.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module EE - module API - module Entities - class GoModuleVersion < Grape::Entity - expose :name, as: 'Version' - expose :time, as: 'Time' - end + module API + module Entities + class GoModuleVersion < Grape::Entity + expose :name, as: 'Version' + expose :time, as: 'Time' end end end +end -- GitLab From 2a9f40a661cb2bd68f1424404d148ed4cce70baa Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Sun, 22 Mar 2020 20:49:31 -0500 Subject: [PATCH 04/19] Implement spec for Go modules proxy --- ee/app/models/packages/go_module_version.rb | 2 +- ee/spec/requests/api/go_proxy_spec.rb | 353 ++++++++++++++++++++ 2 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 ee/spec/requests/api/go_proxy_spec.rb diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index 7c3830d479dc57..4eead23c09f802 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -18,7 +18,7 @@ def initialize(mod, tag) def gomod return @gomod unless @gomod.nil? - blob = @mod.project.repository.blob_at(tag.dereferenced_target.sha, @mod.path + '/go.mod') + blob = @mod.project.repository.blob_at(@tag.dereferenced_target.sha, @mod.path + '/go.mod') @gomod = blob ? blob.data : '' end diff --git a/ee/spec/requests/api/go_proxy_spec.rb b/ee/spec/requests/api/go_proxy_spec.rb new file mode 100644 index 00000000000000..dbe179e1c121da --- /dev/null +++ b/ee/spec/requests/api/go_proxy_spec.rb @@ -0,0 +1,353 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::GoProxy do + let_it_be(:user) { create :user } + let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } + let_it_be(:base) { module_base project } + + let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user } + let_it_be(:job) { create :ci_build, user: user } + let_it_be(:pa_token) { create :personal_access_token, user: user } + + let_it_be(:modules) do + create_version(1, 0, 0, create_readme) + create_version(1, 0, 1, create_module) + create_version(1, 0, 2, create_package('pkg')) + create_version(1, 0, 3, create_module('mod')) + + project.repository.head_commit + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + # context 'with a private project', visibility: 'private' do + # let(:module_name) { base } + + # it_behaves_like 'a module that requires auth' + # end + + # context 'with a public project', visibility: 'public' do + # let(:module_name) { base } + + # it_behaves_like 'a module that does not require auth' + # end + + context 'for the root module' do + let(:module_name) { base } + + it 'returns v1.0.1, v1.0.2, v1.0.3' do + get_resource(user) + + expect_module_version_list('v1.0.1', 'v1.0.2', 'v1.0.3') + end + end + + context 'for the package' do + let(:module_name) { "#{base}/pkg" } + + it 'returns nothing' do + get_resource(user) + + expect_module_version_list + end + end + + context 'for the submodule' do + let(:module_name) { "#{base}/mod" } + + it 'returns v1.0.3' do + get_resource(user) + + expect_module_version_list('v1.0.3') + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with the root module v1.0.1' do + let(:module_name) { base } + let(:resource) { "v1.0.1.info" } + + it 'returns correct information' do + get_resource(user) + + expect_module_version_info('v1.0.1') + end + end + + context 'with the submodule v1.0.3' do + let(:module_name) { "#{base}/mod" } + let(:resource) { "v1.0.3.info" } + + it 'returns correct information' do + get_resource(user) + + expect_module_version_info('v1.0.3') + end + end + + context 'with an invalid path' do + let(:module_name) { "#{base}/pkg" } + let(:resource) { "v1.0.3.info" } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with an invalid version' do + let(:module_name) { "#{base}/mod" } + let(:resource) { "v1.0.1.info" } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.mod' do + context 'with the root module v1.0.1' do + let(:module_name) { base } + let(:resource) { "v1.0.1.mod" } + + it 'returns correct content' do + get_resource(user) + + expect_module_version_mod(module_name) + end + end + + context 'with the submodule v1.0.3' do + let(:module_name) { "#{base}/mod" } + let(:resource) { "v1.0.3.mod" } + + it 'returns correct content' do + get_resource(user) + + expect_module_version_mod(module_name) + end + end + + context 'with an invalid path' do + let(:module_name) { "#{base}/pkg" } + let(:resource) { "v1.0.3.mod" } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with an invalid version' do + let(:module_name) { "#{base}/mod" } + let(:resource) { "v1.0.1.mod" } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.zip' do + context 'with the root module v1.0.1' do + let(:module_name) { base } + let(:resource) { "v1.0.1.zip" } + + it 'returns a zip of everything' do + get_resource(user) + + expect_module_version_zip(Set['README.md', 'go.mod', 'a.go']) + end + end + + context 'with the root module v1.0.2' do + let(:module_name) { base } + let(:resource) { "v1.0.2.zip" } + + it 'returns a zip of everything' do + get_resource(user) + + expect_module_version_zip(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) + end + end + + context 'with the root module v1.0.3' do + let(:module_name) { base } + let(:resource) { "v1.0.3.zip" } + + it 'returns a zip of everything, excluding the submodule' do + get_resource(user) + + expect_module_version_zip(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) + end + end + + context 'with the submodule v1.0.3' do + let(:module_name) { "#{base}/mod" } + let(:resource) { "v1.0.3.zip" } + + it 'returns a zip of the submodule' do + get_resource(user) + + expect_module_version_zip(Set['go.mod', 'a.go']) + end + end + end + + before do + project.add_developer(user) + stub_licensed_features(packages: true) + + modules + end + + shared_context 'has a private project', visibility: 'private' do + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + end + + shared_context 'has a public project', visibility: 'public' do + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + end + + shared_examples 'a module that requires auth' do + it 'returns 200 with oauth token' do + get_resource(access_token: oauth.token) + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns 200 with job token' do + get_resource(job_token: job.token) + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns 200 with personal access token' do + get_resource(personal_access_token: pa_token) + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns 404 with no authentication' do + get_resource + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'a module that does not require auth' do + it 'returns 200 with no authentication' do + get_resource + expect(response).to have_gitlab_http_status(:ok) + end + end + + def get_resource(user = nil, **params) + get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user), params: params + end + + def module_base(project) + Gitlab::Routing.url_helpers.project_url(project).split('://', 2)[1] + end + + def create_readme(commit_message: 'Add README.md') + get_result("create readme", Files::CreateService.new( + project, + project.owner, + commit_message: 'Add README.md', + start_branch: 'master', + branch_name: 'master', + file_path: 'README.md', + file_content: 'Hi' + ).execute) + end + + def create_module(path = '', commit_message: 'Add module') + name = module_base(project) + if path != '' + name += '/' + path + path += '/' + end + + get_result("create module '#{name}'", ::Files::MultiService.new( + project, + project.owner, + commit_message: commit_message, + start_branch: project.repository.root_ref, + branch_name: project.repository.root_ref, + actions: [ + { action: :create, file_path: path + 'go.mod', content: "module #{name}\n" }, + { action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" } + ] + ).execute) + end + + def create_package(path, commit_message: 'Add package') + get_result("create package '#{path}'", ::Files::MultiService.new( + project, + project.owner, + commit_message: commit_message, + start_branch: project.repository.root_ref, + branch_name: project.repository.root_ref, + actions: [ + { action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" } + ] + ).execute) + end + + def create_version(major, minor, patch, sha, prerelease: nil, build: nil, tag_message: nil) + name = "v#{major}.#{minor}.#{patch}" + name += "-#{prerelease}" if prerelease + name += "+#{build}" if build + + get_result("create version #{name[1..]}", ::Tags::CreateService.new(project, project.owner).execute(name, sha, tag_message)) + end + + def get_result(op, ret) + raise "#{op} failed: #{ret}" unless ret[:status] == :success + + ret[:result] + end + + def expect_module_version_list(*versions) + expect(response).to have_gitlab_http_status(:ok) + expect(response.body.split("\n")).to eq(versions) + end + + def expect_module_version_info(version) + # time = project.repository.find_tag(version).dereferenced_target.committed_date + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + # expect(Date.parse json_response['Time']).to eq(time) + end + + def expect_module_version_mod(name) + expect(response).to have_gitlab_http_status(:ok) + expect(response.body.split("\n", 2).first).to eq("module #{name}") + end + + def expect_module_version_zip(entries) + expect(response).to have_gitlab_http_status(:ok) + + actual = Set[] + Zip::InputStream.open(StringIO.new(response.body)) do |zip| + while (entry = zip.get_next_entry) + actual.add(entry.name) + end + end + + expect(actual).to eq(entries) + end +end -- GitLab From 575b8495e0b5c0f86bb8983845a43d4a57a586a0 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Tue, 24 Mar 2020 02:23:52 -0500 Subject: [PATCH 05/19] Fix Go module proxy issues - Fix zip entry paths. The Go proxy spec requires zip entries to conform to `module@version/file`, where `file` is the path within the module. - Fix /v2+ handling. For major versions 2+, the module name must include the major version as a suffix, e.g. /v2. - Handle case encoding. Requests to the Go proxy encode uppercase characters in URLs as '!' followed by the character in lowercase. - Per Zoom discussion with @trizzi, @sabrams, and team, modules with an invalid module name in go.mod will be ignored, initially. --- ee/app/models/packages/go_module.rb | 4 +- ee/app/models/packages/go_module_version.rb | 28 ++- ee/lib/api/go_proxy.rb | 35 +-- ee/spec/requests/api/go_proxy_spec.rb | 237 +++++++++++++++++--- 4 files changed, 247 insertions(+), 57 deletions(-) diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb index 2cb7ee9897dc6e..3031430d66df1d 100644 --- a/ee/app/models/packages/go_module.rb +++ b/ee/app/models/packages/go_module.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Packages::GoModule - SEMVER_TAG_REGEX = Regexp.new("^#{::Packages::GoModuleVersion::SEMVER_REGEX.source}$").freeze + SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze # belongs_to :project @@ -22,7 +22,7 @@ def initialize(project, name) end def versions - @versions ||= project.repository.tags + @versions ||= @project.repository.tags .filter { |tag| SEMVER_TAG_REGEX.match?(tag.name) && !tag.dereferenced_target.nil? } .map { |tag| ::Packages::GoModuleVersion.new self, tag } .filter { |ver| ver.valid? } diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index 4eead23c09f802..a29591d4d9b3a5 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Packages::GoModuleVersion - SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.A-Z0-9]+))?(?:\+([-.A-Z0-9]+))?/i.freeze + SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze VERSION_SUFFIX_REGEX = /\/v([1-9]\d*)$/i.freeze # belongs_to :mod @@ -16,22 +16,30 @@ def initialize(mod, tag) end def gomod - return @gomod unless @gomod.nil? - - blob = @mod.project.repository.blob_at(@tag.dereferenced_target.sha, @mod.path + '/go.mod') - @gomod = blob ? blob.data : '' + @gomod ||= @mod.project.repository.blob_at(@tag.dereferenced_target.sha, @mod.path + '/go.mod')&.data end def valid? - m = gomod.split("\n", 2).first + valid_path? && valid_module? + end + + def valid_path? + m = VERSION_SUFFIX_REGEX.match(@mod.name) + case major when 0, 1 - m == "module #{@mod.name}" + m.nil? else - m == "module #{@mod.name}/v#{major}" + !m.nil? && m[1].to_i == major end end + def valid_module? + return false unless gomod + + gomod.split("\n", 2).first == "module #{@mod.name}" + end + def major SEMVER_REGEX.match(@tag.name)[1].to_i end @@ -56,8 +64,8 @@ def files return @files unless @files.nil? sha = @tag.dereferenced_target.sha - tree = @mod.project.repository.tree(sha, mod.path, recursive: true).entries.filter { |e| e.file? } - nested = tree.filter { |e| e.name == 'go.mod' && !(mod.path == '' && e.path == 'go.mod' || e.path == mod.path + '/go.mod') }.map { |e| e.path[0..-7] } + tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } + nested = tree.filter { |e| e.name == 'go.mod' && !(@mod.path == '' && e.path == 'go.mod' || e.path == @mod.path + '/go.mod') }.map { |e| e.path[0..-7] } @files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index 11c55278b9c8c8..81c66d773a206b 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -3,13 +3,18 @@ module API class GoProxy < Grape::API helpers ::API::Helpers::PackagesHelpers - MODULE_VERSION_REQUIREMENTS = { module_version: ::Packages::GoModuleVersion::SEMVER_REGEX }.freeze + MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze + MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze before { require_packages_enabled! } helpers do + def case_decode(str) + str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase } + end + def find_module - module_name = params[:module_name].gsub(/![[:alpha:]]/) { |s| s[1..].upcase } + module_name = case_decode params[:module_name] bad_request!('Module Name') if module_name.blank? @@ -18,6 +23,15 @@ def find_module mod end + + def find_version + mod = find_module + + ver = mod.find_version case_decode params[:module_version] + not_found! unless ver + + ver + end end params do @@ -49,10 +63,7 @@ def find_module requires :module_version, type: String, desc: 'Module version' end get ':module_version.info', requirements: MODULE_VERSION_REQUIREMENTS do - mod = find_module - - ver = mod.find_version params[:module_version] - not_found! unless ver + ver = find_version present ::Packages::Go::ModuleVersionPresenter.new(ver), with: EE::API::Entities::GoModuleVersion end @@ -64,10 +75,7 @@ def find_module requires :module_version, type: String, desc: 'Module version' end get ':module_version.mod', requirements: MODULE_VERSION_REQUIREMENTS do - mod = find_module - - ver = mod.find_version params[:module_version] - not_found! unless ver + ver = find_version content_type 'text/plain' ver.gomod @@ -80,14 +88,13 @@ def find_module requires :module_version, type: String, desc: 'Module version' end get ':module_version.zip', requirements: MODULE_VERSION_REQUIREMENTS do - mod = find_module + ver = find_version - ver = mod.find_version params[:module_version] - not_found! unless ver + suffix_len = ver.mod.path == '' ? 0 : ver.mod.path.length + 1 s = Zip::OutputStream.write_buffer do |zip| ver.files.each do |file| - zip.put_next_entry file.path + zip.put_next_entry "#{ver.mod.name}@#{ver.name}/#{file.path[suffix_len...]}" zip.write ver.blob_at(file.path) end end diff --git a/ee/spec/requests/api/go_proxy_spec.rb b/ee/spec/requests/api/go_proxy_spec.rb index dbe179e1c121da..cc328f63988b61 100644 --- a/ee/spec/requests/api/go_proxy_spec.rb +++ b/ee/spec/requests/api/go_proxy_spec.rb @@ -3,23 +3,158 @@ require 'spec_helper' describe API::GoProxy do + let_it_be(:domain) do + port = ::Gitlab.config.gitlab.port + host = ::Gitlab.config.gitlab.host + case port when 80, 443 then host else "#{host}:#{port}" end + end + let_it_be(:user) { create :user } let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } - let_it_be(:base) { module_base project } + let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user } let_it_be(:job) { create :ci_build, user: user } let_it_be(:pa_token) { create :personal_access_token, user: user } + # rubocop: disable Layout/IndentationConsistency let_it_be(:modules) do - create_version(1, 0, 0, create_readme) + create_version(1, 0, 0, create_file('README.md', 'Hi', commit_message: 'Add README.md')) create_version(1, 0, 1, create_module) create_version(1, 0, 2, create_package('pkg')) create_version(1, 0, 3, create_module('mod')) + create_module('v2') + create_version(2, 0, 0, create_file('v2/x.go', "package a\n")) + create_file('v2/y.go', "package a\n") # todo tag this v1.0.4 project.repository.head_commit end + context 'with an invalid module directive' do + let_it_be(:project) { create :project_empty_repo, :public, creator: user } + let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } + + # rubocop: disable Layout/IndentationWidth + let_it_be(:modules) do + create_file('a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") + create_version(1, 0, 0, create_file('go.mod', "module not/a/real/module\n")) + create_file('v2/a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") + create_version(2, 0, 0, create_file('v2/go.mod', "module #{base}\n")) + + project.repository.head_commit + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + context 'with a completely wrong directive for v1' do + let(:module_name) { base } + + it 'returns nothing' do + get_resource(user) + + expect_module_version_list + end + end + + context 'with a directive omitting the suffix for v2' do + let(:module_name) { "#{base}/v2" } + + it 'returns nothing' do + get_resource(user) + + expect_module_version_list + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with a completely wrong directive for v1' do + let(:module_name) { base } + let(:resource) { "v1.0.0.info" } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with a directive omitting the suffix for v2' do + let(:module_name) { "#{base}/v2" } + let(:resource) { "v2.0.0.info" } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + context 'with a case sensitive project and versions' do + let_it_be(:project) { create :project_empty_repo, :public, creator: user, path: 'MyGoLib' } + let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } + let_it_be(:base_encoded) { base.gsub(/[A-Z]/) { |s| "!#{s.downcase}"} } + + let_it_be(:modules) do + create_file('README.md', 'Hi', commit_message: 'Add README.md') + create_version(1, 0, 1, create_module, prerelease: 'prerelease') + create_version(1, 0, 1, create_package('pkg'), prerelease: 'Prerelease') + + project.repository.head_commit + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + context 'with a case encoded path' do + let(:module_name) { base_encoded } + + it 'returns the tags' do + get_resource(user) + + expect_module_version_list('v1.0.1-prerelease', 'v1.0.1-Prerelease') + end + end + + context 'without a case encoded path' do + let(:module_name) { base.downcase } + + it 'returns 404' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with a case encoded path' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-!prerelease.info" } + + it 'returns the uppercase tag' do + get_resource(user) + + expect_module_version_info('v1.0.1-Prerelease') + end + end + + context 'without a case encoded path' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-prerelease.info" } + + it 'returns the lowercase tag' do + get_resource(user) + + expect_module_version_info('v1.0.1-prerelease') + end + end + end + end + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do let(:resource) { "list" } @@ -64,6 +199,16 @@ expect_module_version_list('v1.0.3') end end + + context 'for the root module v2' do + let(:module_name) { "#{base}/v2" } + + it 'returns v2.0.0' do + get_resource(user) + + expect_module_version_list('v2.0.0') + end + end end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do @@ -89,6 +234,17 @@ end end + context 'with the root module v2.0.0' do + let(:module_name) { "#{base}/v2" } + let(:resource) { "v2.0.0.info" } + + it 'returns correct information' do + get_resource(user) + + expect_module_version_info('v2.0.0') + end + end + context 'with an invalid path' do let(:module_name) { "#{base}/pkg" } let(:resource) { "v1.0.3.info" } @@ -135,6 +291,17 @@ end end + context 'with the root module v2.0.0' do + let(:module_name) { "#{base}/v2" } + let(:resource) { "v2.0.0.mod" } + + it 'returns correct content' do + get_resource(user) + + expect_module_version_mod(module_name) + end + end + context 'with an invalid path' do let(:module_name) { "#{base}/pkg" } let(:resource) { "v1.0.3.mod" } @@ -166,7 +333,7 @@ it 'returns a zip of everything' do get_resource(user) - expect_module_version_zip(Set['README.md', 'go.mod', 'a.go']) + expect_module_version_zip(module_name, 'v1.0.1', ['README.md', 'go.mod', 'a.go']) end end @@ -177,7 +344,7 @@ it 'returns a zip of everything' do get_resource(user) - expect_module_version_zip(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) + expect_module_version_zip(module_name, 'v1.0.2', ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) end end @@ -188,7 +355,7 @@ it 'returns a zip of everything, excluding the submodule' do get_resource(user) - expect_module_version_zip(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) + expect_module_version_zip(module_name, 'v1.0.3', ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) end end @@ -199,7 +366,18 @@ it 'returns a zip of the submodule' do get_resource(user) - expect_module_version_zip(Set['go.mod', 'a.go']) + expect_module_version_zip(module_name, 'v1.0.3', ['go.mod', 'a.go']) + end + end + + context 'with the root module v2.0.0' do + let(:module_name) { "#{base}/v2" } + let(:resource) { "v2.0.0.zip" } + + it 'returns a zip of v2 of the root module' do + get_resource(user) + + expect_module_version_zip(module_name, 'v2.0.0', ['go.mod', 'a.go', 'x.go']) end end end @@ -256,51 +434,47 @@ def get_resource(user = nil, **params) get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user), params: params end - def module_base(project) - Gitlab::Routing.url_helpers.project_url(project).split('://', 2)[1] - end - - def create_readme(commit_message: 'Add README.md') - get_result("create readme", Files::CreateService.new( + def create_file(path, content, commit_message: 'Add file') + get_result("create file", Files::CreateService.new( project, project.owner, - commit_message: 'Add README.md', + commit_message: commit_message, start_branch: 'master', branch_name: 'master', - file_path: 'README.md', - file_content: 'Hi' + file_path: path, + file_content: content ).execute) end - def create_module(path = '', commit_message: 'Add module') - name = module_base(project) - if path != '' - name += '/' + path - path += '/' - end - - get_result("create module '#{name}'", ::Files::MultiService.new( + def create_package(path, commit_message: 'Add package') + get_result("create package '#{path}'", Files::MultiService.new( project, project.owner, commit_message: commit_message, start_branch: project.repository.root_ref, branch_name: project.repository.root_ref, actions: [ - { action: :create, file_path: path + 'go.mod', content: "module #{name}\n" }, - { action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" } + { action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" } ] ).execute) end - def create_package(path, commit_message: 'Add package') - get_result("create package '#{path}'", ::Files::MultiService.new( + def create_module(path = '', commit_message: 'Add module') + name = "#{domain}/#{project.path_with_namespace}" + if path != '' + name += '/' + path + path += '/' + end + + get_result("create module '#{name}'", Files::MultiService.new( project, project.owner, commit_message: commit_message, start_branch: project.repository.root_ref, branch_name: project.repository.root_ref, actions: [ - { action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" } + { action: :create, file_path: path + 'go.mod', content: "module #{name}\n" }, + { action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" } ] ).execute) end @@ -310,7 +484,7 @@ def create_version(major, minor, patch, sha, prerelease: nil, build: nil, tag_me name += "-#{prerelease}" if prerelease name += "+#{build}" if build - get_result("create version #{name[1..]}", ::Tags::CreateService.new(project, project.owner).execute(name, sha, tag_message)) + get_result("create version #{name[1..]}", Tags::CreateService.new(project, project.owner).execute(name, sha, tag_message)) end def get_result(op, ret) @@ -321,7 +495,7 @@ def get_result(op, ret) def expect_module_version_list(*versions) expect(response).to have_gitlab_http_status(:ok) - expect(response.body.split("\n")).to eq(versions) + expect(response.body.split("\n").to_set).to eq(versions.to_set) end def expect_module_version_info(version) @@ -338,9 +512,10 @@ def expect_module_version_mod(name) expect(response.body.split("\n", 2).first).to eq("module #{name}") end - def expect_module_version_zip(entries) + def expect_module_version_zip(path, version, entries) expect(response).to have_gitlab_http_status(:ok) + entries = entries.map { |e| "#{path}@#{version}/#{e}" }.to_set actual = Set[] Zip::InputStream.open(StringIO.new(response.body)) do |zip| while (entry = zip.get_next_entry) -- GitLab From 95a3e1a17fc28c5221298b46f5ded397e082b7a6 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Thu, 26 Mar 2020 02:52:08 -0500 Subject: [PATCH 06/19] Implement Go module proxy pseudo-versions --- ee/app/models/packages/go_module.rb | 16 +- ee/app/models/packages/go_module_version.rb | 105 +++++++++--- .../packages/go/module_version_presenter.rb | 2 +- ee/lib/api/go_proxy.rb | 2 +- ee/spec/requests/api/go_proxy_spec.rb | 152 +++++++++++++----- 5 files changed, 203 insertions(+), 74 deletions(-) diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb index 3031430d66df1d..4f9961bcae9b2a 100644 --- a/ee/app/models/packages/go_module.rb +++ b/ee/app/models/packages/go_module.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Packages::GoModule - SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze - - # belongs_to :project - attr_reader :project, :name, :path def initialize(project, name) @@ -23,13 +19,21 @@ def initialize(project, name) def versions @versions ||= @project.repository.tags - .filter { |tag| SEMVER_TAG_REGEX.match?(tag.name) && !tag.dereferenced_target.nil? } + .filter { |tag| ::Packages::GoModuleVersion.semver? tag } .map { |tag| ::Packages::GoModuleVersion.new self, tag } .filter { |ver| ver.valid? } end def find_version(name) - versions.filter { |ver| ver.name == name }.first + if ::Packages::GoModuleVersion.pseudo_version? name + begin + ::Packages::GoModuleVersion.new self, name + rescue ArgumentError + nil + end + else + versions.filter { |ver| ver.name == name }.first + end end private diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index a29591d4d9b3a5..f65041c1af837f 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -2,21 +2,77 @@ class Packages::GoModuleVersion SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze + SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze + PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze VERSION_SUFFIX_REGEX = /\/v([1-9]\d*)$/i.freeze - # belongs_to :mod + attr_reader :mod, :type, :ref, :commit - attr_reader :mod, :tag + delegate :major, to: :@semver, allow_nil: true + delegate :minor, to: :@semver, allow_nil: true + delegate :patch, to: :@semver, allow_nil: true + delegate :prerelease, to: :@semver, allow_nil: true + delegate :build, to: :@semver, allow_nil: true - delegate :name, to: :tag + def self.semver?(tag) + return false if tag.dereferenced_target.nil? - def initialize(mod, tag) + SEMVER_TAG_REGEX.match?(tag.name) + end + + def self.pseudo_version?(str) + SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str) + end + + def initialize(mod, target) @mod = mod - @tag = tag + + case target + when String + m = SEMVER_TAG_REGEX.match(target) + raise ArgumentError.new 'target is not a pseudo-version' unless m && PSEUDO_VERSION_REGEX.match?(target) + + # valid pseudo-versions are + # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X + # vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre + # vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z + + # go discards the timestamp when resolving pseudo-versions, so we will do the same + + @type = :pseudo + @name = target + @semver = semver_match_to_hash m + + timestamp, sha = prerelease.split('-').last 2 + timestamp = timestamp.split('.').last + @commit = mod.project.repository.commit_by(oid: sha) + + # these errors are copied from proxy.golang.org's responses + raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless @commit + raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12 + raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless @commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp + + when Gitlab::Git::Ref + @type = :ref + @ref = target + @commit = target.dereferenced_target + @semver = semver_match_to_hash SEMVER_TAG_REGEX.match(target.name) + + when ::Commit, Gitlab::Git::Commit + @type = :commit + @commit = target + + else + raise ArgumentError.new 'not a valid target' + end + end + + def name + @name || @ref&.name end def gomod - @gomod ||= @mod.project.repository.blob_at(@tag.dereferenced_target.sha, @mod.path + '/go.mod')&.data + @gomod ||= @mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data end def valid? @@ -40,36 +96,33 @@ def valid_module? gomod.split("\n", 2).first == "module #{@mod.name}" end - def major - SEMVER_REGEX.match(@tag.name)[1].to_i - end - - def minor - SEMVER_REGEX.match(@tag.name)[2].to_i - end - - def patch - SEMVER_REGEX.match(@tag.name)[3].to_i - end - - def prerelease - SEMVER_REGEX.match(@tag.name)[4] - end - - def build - SEMVER_REGEX.match(@tag.name)[5] + def pseudo? + @type == :pseudo end def files return @files unless @files.nil? - sha = @tag.dereferenced_target.sha + sha = @commit.sha tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } nested = tree.filter { |e| e.name == 'go.mod' && !(@mod.path == '' && e.path == 'go.mod' || e.path == @mod.path + '/go.mod') }.map { |e| e.path[0..-7] } @files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } end def blob_at(path) - @mod.project.repository.blob_at(tag.dereferenced_target.sha, path).data + @mod.project.repository.blob_at(@commit.sha, path).data + end + + private + + def semver_match_to_hash(match) + return unless match + + OpenStruct.new( + major: match[1].to_i, + minor: match[2].to_i, + patch: match[3].to_i, + prerelease: match[4], + build: match[5]) end end diff --git a/ee/app/presenters/packages/go/module_version_presenter.rb b/ee/app/presenters/packages/go/module_version_presenter.rb index 9e160406e070d2..4c86eae46cd193 100644 --- a/ee/app/presenters/packages/go/module_version_presenter.rb +++ b/ee/app/presenters/packages/go/module_version_presenter.rb @@ -12,7 +12,7 @@ def name end def time - @version.tag.dereferenced_target.committed_date + @version.commit.committed_date end end end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index 81c66d773a206b..a53cedbf9de934 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -28,7 +28,7 @@ def find_version mod = find_module ver = mod.find_version case_decode params[:module_version] - not_found! unless ver + not_found! unless ver&.valid? ver end diff --git a/ee/spec/requests/api/go_proxy_spec.rb b/ee/spec/requests/api/go_proxy_spec.rb index cc328f63988b61..2dcc4b70f09bcf 100644 --- a/ee/spec/requests/api/go_proxy_spec.rb +++ b/ee/spec/requests/api/go_proxy_spec.rb @@ -23,11 +23,11 @@ create_version(1, 0, 1, create_module) create_version(1, 0, 2, create_package('pkg')) create_version(1, 0, 3, create_module('mod')) - create_module('v2') + sha1 = create_file('y.go', "package a\n") + sha2 = create_module('v2') create_version(2, 0, 0, create_file('v2/x.go', "package a\n")) - create_file('v2/y.go', "package a\n") # todo tag this v1.0.4 - project.repository.head_commit + { sha: [sha1, sha2] } end context 'with an invalid module directive' do @@ -73,7 +73,7 @@ let(:module_name) { base } let(:resource) { "v1.0.0.info" } - it 'returns 404' do + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -84,7 +84,7 @@ let(:module_name) { "#{base}/v2" } let(:resource) { "v2.0.0.info" } - it 'returns 404' do + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -122,7 +122,7 @@ context 'without a case encoded path' do let(:module_name) { base.downcase } - it 'returns 404' do + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -212,44 +212,46 @@ end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + let(:resource) { "#{version}.info" } + context 'with the root module v1.0.1' do let(:module_name) { base } - let(:resource) { "v1.0.1.info" } + let(:version) { "v1.0.1" } it 'returns correct information' do get_resource(user) - expect_module_version_info('v1.0.1') + expect_module_version_info(version) end end context 'with the submodule v1.0.3' do let(:module_name) { "#{base}/mod" } - let(:resource) { "v1.0.3.info" } + let(:version) { "v1.0.3" } it 'returns correct information' do get_resource(user) - expect_module_version_info('v1.0.3') + expect_module_version_info(version) end end context 'with the root module v2.0.0' do let(:module_name) { "#{base}/v2" } - let(:resource) { "v2.0.0.info" } + let(:version) { "v2.0.0" } it 'returns correct information' do get_resource(user) - expect_module_version_info('v2.0.0') + expect_module_version_info(version) end end context 'with an invalid path' do let(:module_name) { "#{base}/pkg" } - let(:resource) { "v1.0.3.info" } + let(:version) { "v1.0.3" } - it 'returns 404' do + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -258,9 +260,75 @@ context 'with an invalid version' do let(:module_name) { "#{base}/mod" } - let(:resource) { "v1.0.1.info" } + let(:version) { "v1.0.1" } - it 'returns 404' do + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with a pseudo-version for v1' do + let(:module_name) { base } + let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" } + + it 'returns the correct commit' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + expect(json_response['Time']).to eq(commit.committed_date.strftime '%Y-%m-%dT%H:%M:%S.%L%:z') + end + end + + context 'with a pseudo-version for v2' do + let(:module_name) { "#{base}/v2" } + let(:commit) { project.repository.commit_by(oid: modules[:sha][1]) } + let(:version) { "v2.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" } + + it 'returns the correct commit' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + expect(json_response['Time']).to eq(commit.committed_date.strftime '%Y-%m-%dT%H:%M:%S.%L%:z') + end + end + + context 'with a pseudo-version with an invalid timestamp' do + let(:module_name) { base } + let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } + let(:version) { "v1.0.4-0.00000000000000-#{commit.sha[0..11]}" } + + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with a pseudo-version with an invalid commit sha' do + let(:module_name) { base } + let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-000000000000" } + + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with a pseudo-version with a short commit sha' do + let(:module_name) { base } + let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..10]}" } + + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -269,9 +337,11 @@ end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.mod' do + let(:resource) { "#{version}.mod" } + context 'with the root module v1.0.1' do let(:module_name) { base } - let(:resource) { "v1.0.1.mod" } + let(:version) { "v1.0.1" } it 'returns correct content' do get_resource(user) @@ -282,7 +352,7 @@ context 'with the submodule v1.0.3' do let(:module_name) { "#{base}/mod" } - let(:resource) { "v1.0.3.mod" } + let(:version) { "v1.0.3" } it 'returns correct content' do get_resource(user) @@ -293,7 +363,7 @@ context 'with the root module v2.0.0' do let(:module_name) { "#{base}/v2" } - let(:resource) { "v2.0.0.mod" } + let(:version) { "v2.0.0" } it 'returns correct content' do get_resource(user) @@ -304,9 +374,9 @@ context 'with an invalid path' do let(:module_name) { "#{base}/pkg" } - let(:resource) { "v1.0.3.mod" } + let(:version) { "v1.0.3" } - it 'returns 404' do + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -315,9 +385,9 @@ context 'with an invalid version' do let(:module_name) { "#{base}/mod" } - let(:resource) { "v1.0.1.mod" } + let(:version) { "v1.0.1" } - it 'returns 404' do + it 'returns not found' do get_resource(user) expect(response).to have_gitlab_http_status(:not_found) @@ -326,58 +396,60 @@ end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.zip' do + let(:resource) { "#{version}.zip" } + context 'with the root module v1.0.1' do let(:module_name) { base } - let(:resource) { "v1.0.1.zip" } + let(:version) { "v1.0.1" } it 'returns a zip of everything' do get_resource(user) - expect_module_version_zip(module_name, 'v1.0.1', ['README.md', 'go.mod', 'a.go']) + expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go']) end end context 'with the root module v1.0.2' do let(:module_name) { base } - let(:resource) { "v1.0.2.zip" } + let(:version) { "v1.0.2" } it 'returns a zip of everything' do get_resource(user) - expect_module_version_zip(module_name, 'v1.0.2', ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) + expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) end end context 'with the root module v1.0.3' do let(:module_name) { base } - let(:resource) { "v1.0.3.zip" } + let(:version) { "v1.0.3" } it 'returns a zip of everything, excluding the submodule' do get_resource(user) - expect_module_version_zip(module_name, 'v1.0.3', ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) + expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) end end context 'with the submodule v1.0.3' do let(:module_name) { "#{base}/mod" } - let(:resource) { "v1.0.3.zip" } + let(:version) { "v1.0.3" } it 'returns a zip of the submodule' do get_resource(user) - expect_module_version_zip(module_name, 'v1.0.3', ['go.mod', 'a.go']) + expect_module_version_zip(module_name, version, ['go.mod', 'a.go']) end end context 'with the root module v2.0.0' do let(:module_name) { "#{base}/v2" } - let(:resource) { "v2.0.0.zip" } + let(:version) { "v2.0.0" } it 'returns a zip of v2 of the root module' do get_resource(user) - expect_module_version_zip(module_name, 'v2.0.0', ['go.mod', 'a.go', 'x.go']) + expect_module_version_zip(module_name, version, ['go.mod', 'a.go', 'x.go']) end end end @@ -402,29 +474,29 @@ end shared_examples 'a module that requires auth' do - it 'returns 200 with oauth token' do + it 'returns ok with oauth token' do get_resource(access_token: oauth.token) expect(response).to have_gitlab_http_status(:ok) end - it 'returns 200 with job token' do + it 'returns ok with job token' do get_resource(job_token: job.token) expect(response).to have_gitlab_http_status(:ok) end - it 'returns 200 with personal access token' do + it 'returns ok with personal access token' do get_resource(personal_access_token: pa_token) expect(response).to have_gitlab_http_status(:ok) end - it 'returns 404 with no authentication' do + it 'returns not found with no authentication' do get_resource expect(response).to have_gitlab_http_status(:not_found) end end shared_examples 'a module that does not require auth' do - it 'returns 200 with no authentication' do + it 'returns ok with no authentication' do get_resource expect(response).to have_gitlab_http_status(:ok) end @@ -499,12 +571,12 @@ def expect_module_version_list(*versions) end def expect_module_version_info(version) - # time = project.repository.find_tag(version).dereferenced_target.committed_date + time = project.repository.find_tag(version).dereferenced_target.committed_date expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_kind_of(Hash) expect(json_response['Version']).to eq(version) - # expect(Date.parse json_response['Time']).to eq(time) + expect(json_response['Time']).to eq(time.strftime '%Y-%m-%dT%H:%M:%S.%L%:z') end def expect_module_version_mod(name) -- GitLab From f96bd4bb8ba5162218875bf3ca9ed875a28dbb0b Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Thu, 26 Mar 2020 13:48:21 -0500 Subject: [PATCH 07/19] Document Go module proxy --- doc/user/packages/go_proxy/index.md | 69 ++++++++++++++++++++++++++++- doc/user/packages/index.md | 2 +- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index b9e78566c31224..3d235f832c3284 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -5,8 +5,9 @@ The GitLab Go Proxy implements the Go proxy protocol. NOTE: **Note:** -GitLab does not (yet) display Go modules in the **Packages** section of a project. -Only the Go proxy protocol is supported at this time. +GitLab does not (yet) display Go modules in the **Packages** section of a +project. Only the Go proxy protocol is supported at this time, and only for +modules on GitLab. ## Enabling the Go proxy @@ -22,3 +23,67 @@ by default. To enable it for existing projects, or if you want to disable it: 1. Click on **Save changes** for the changes to take effect. You should then be able to see the **Packages** section on the left sidebar. +Next, you must configure your development environment to use the Go proxy. + +## Adding GitLab as a Go proxy + +NOTE: **Note:** +To use a Go proxy, you must be using Go 1.13 or later. + +The available proxy endpoints are: + +- Project - can fetch modules defined by a project - `/api/v4/projects/:id/packages/go` + +Go's use of proxies is configured with the `GOPROXY` environment variable, as a +comma separated list of URLs. Go 1.14 adds support for managing Go's environment +variables via `go env -w`, e.g. `go env -w GOPROXY=...`. This will write to +`$GOPATH/env` (which defaults to `~/.go/env`). `GOPROXY` can also be configured +as a normal environment variable, via RC files or `export GOPROXY=...`. + +The default value of `$GOPROXY` is `https://proxy.golang.org,direct`, which +tells `go` to first query `proxy.golang.org` and fallback to direct VCS +operations (`git clone`, `svc checkout`, etc). Replacing +`https://proxy.golang.org` with a GitLab endpoint will direct all fetches +through GitLab. Currently GitLab's Go proxy does not support dependency +proxying, so all external dependencies will be handled directly. If GitLab's +endpoint is inserted before `https://proxy.golang.org`, then all fetches will +first go through GitLab. This can help avoid making requests for private +packages to the public proxy, but `GOPRIVATE` is a much safer way of achieving +that. + +## Releasing a module + +NOTE: **Note:** +For a complete understanding of Go modules and versioning, see [this series of +blog posts](https://blog.golang.org/using-go-modules) on the official Go +website. + +Go modules and module versions are handled entirely via Git (or SVN, Mercurial, +etc). A module is a repository containing Go source and a `go.mod` file. A +version of a module is a Git tag (or equivalent) that is a valid [semantic +version](https://semver.org), prefixed with 'v'. For example, `v1.0.0` and +`v1.3.2-alpha` are valid module versions, but `v1` or `v1.2` are not. + +Go requires that major versions after v1 involve a change in the import path of +the module. For example, version 2 of the module `gitlab.com/my/project` must be +imported and released as `gitlab.com/my/project/v2`. + +## Valid modules and versions + +The GitLab Go proxy will ignore modules and module versions that have an invalid +`module` directive in their `go.mod`. Go requires that a package imported as +`gitlab.com/my/project` can be accessed via that same URL, and that the first +line of `go.mod` is `module gitlab.com/my/project`. If `go.mod` names a +different module, compilation will fail. Additionally, Go requires, for major +versions after 1, that the name of the module have an appropriate suffix, e.g. +`gitlab.com/my/project/v2`. If the `module` directive does not also have this +suffix, compilation will fail. + +Go supports 'pseudo-versions' that encode the timestamp and SHA of a commit. +Tags that match the pseudo-version pattern are ignored, as otherwise they could +interfere with fetching specific commits using a pseudo-version. Pseudo-versions +follow one of three formats: + +- `vX.0.0-yyyymmddhhmmss-abcdefabcdef`, when no earlier tagged commit exists for X +- `vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z-pre +- `vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index 9cc14faa48f748..e0f3a19ae94807 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -21,6 +21,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [NPM Registry](npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | | [NuGet Repository](nuget_repository/index.md) **(PREMIUM)** | The GitLab NuGet Repository will enable every project in GitLab to have its own space to store [NuGet](https://www.nuget.org/) packages. | 12.8+ | | [PyPi Repository](pypi_repository/index.md) **(PREMIUM)** | The GitLab PyPi Repository will enable every project in GitLab to have its own space to store [PyPi](https://pypi.org/) packages. | 12.10+ | +| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | ??.??+ | ## Enable the Package Registry for your project @@ -116,7 +117,6 @@ are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/-/merge_reques | [Conda](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | Secure and private local Conda repositories. | | [CRAN](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) | Deploy and resolve CRAN packages for the R language. | | [Debian](https://gitlab.com/gitlab-org/gitlab/-/issues/5835) | Host and provision Debian packages. | -| [Go](https://gitlab.com/gitlab-org/gitlab/-/issues/9773) | Resolve Go dependencies from and publish your Go packages to GitLab. | | [Opkg](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) | Optimize your work with OpenWrt using Opkg repositories. | | [P2](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) | Host all your Eclipse plugins in your own GitLab P2 repository. | | [Puppet](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) | Configuration management meets repository management with Puppet repositories. | -- GitLab From 30232ef88a308245da8f0ba59bf8f669f914e39b Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Tue, 31 Mar 2020 00:21:26 -0500 Subject: [PATCH 08/19] Update authn/authz for Go module proxy - Allow CI job tokens - Return 401 when project is not public and request is not authenticated - Add assertions to spec to verify authn/authz --- ee/lib/api/go_proxy.rb | 16 ++ ee/spec/requests/api/go_proxy_spec.rb | 359 ++++++++++++++------------ 2 files changed, 210 insertions(+), 165 deletions(-) diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index a53cedbf9de934..16f8423e246ea4 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -32,12 +32,28 @@ def find_version ver end + + # override :find_project! + def find_project!(id) + project = find_project(id) + + ability = job_token_authentication? ? :build_read_project : :read_project + + if can?(current_user, ability, project) + project + elsif current_user.nil? + unauthorized! + else + not_found!('Project') + end + end end params do requires :id, type: String, desc: 'The ID of a project' requires :module_name, type: String, desc: 'Module name' end + route_setting :authentication, job_token_allowed: true resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before do authorize_read_package! diff --git a/ee/spec/requests/api/go_proxy_spec.rb b/ee/spec/requests/api/go_proxy_spec.rb index 2dcc4b70f09bcf..66fe413e94884c 100644 --- a/ee/spec/requests/api/go_proxy_spec.rb +++ b/ee/spec/requests/api/go_proxy_spec.rb @@ -30,146 +30,9 @@ { sha: [sha1, sha2] } end - context 'with an invalid module directive' do - let_it_be(:project) { create :project_empty_repo, :public, creator: user } - let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } - - # rubocop: disable Layout/IndentationWidth - let_it_be(:modules) do - create_file('a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") - create_version(1, 0, 0, create_file('go.mod', "module not/a/real/module\n")) - create_file('v2/a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") - create_version(2, 0, 0, create_file('v2/go.mod', "module #{base}\n")) - - project.repository.head_commit - end - - describe 'GET /projects/:id/packages/go/*module_name/@v/list' do - let(:resource) { "list" } - - context 'with a completely wrong directive for v1' do - let(:module_name) { base } - - it 'returns nothing' do - get_resource(user) - - expect_module_version_list - end - end - - context 'with a directive omitting the suffix for v2' do - let(:module_name) { "#{base}/v2" } - - it 'returns nothing' do - get_resource(user) - - expect_module_version_list - end - end - end - - describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do - context 'with a completely wrong directive for v1' do - let(:module_name) { base } - let(:resource) { "v1.0.0.info" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'with a directive omitting the suffix for v2' do - let(:module_name) { "#{base}/v2" } - let(:resource) { "v2.0.0.info" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - - context 'with a case sensitive project and versions' do - let_it_be(:project) { create :project_empty_repo, :public, creator: user, path: 'MyGoLib' } - let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } - let_it_be(:base_encoded) { base.gsub(/[A-Z]/) { |s| "!#{s.downcase}"} } - - let_it_be(:modules) do - create_file('README.md', 'Hi', commit_message: 'Add README.md') - create_version(1, 0, 1, create_module, prerelease: 'prerelease') - create_version(1, 0, 1, create_package('pkg'), prerelease: 'Prerelease') - - project.repository.head_commit - end - - describe 'GET /projects/:id/packages/go/*module_name/@v/list' do - let(:resource) { "list" } - - context 'with a case encoded path' do - let(:module_name) { base_encoded } - - it 'returns the tags' do - get_resource(user) - - expect_module_version_list('v1.0.1-prerelease', 'v1.0.1-Prerelease') - end - end - - context 'without a case encoded path' do - let(:module_name) { base.downcase } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do - context 'with a case encoded path' do - let(:module_name) { base_encoded } - let(:resource) { "v1.0.1-!prerelease.info" } - - it 'returns the uppercase tag' do - get_resource(user) - - expect_module_version_info('v1.0.1-Prerelease') - end - end - - context 'without a case encoded path' do - let(:module_name) { base_encoded } - let(:resource) { "v1.0.1-prerelease.info" } - - it 'returns the lowercase tag' do - get_resource(user) - - expect_module_version_info('v1.0.1-prerelease') - end - end - end - end - describe 'GET /projects/:id/packages/go/*module_name/@v/list' do let(:resource) { "list" } - # context 'with a private project', visibility: 'private' do - # let(:module_name) { base } - - # it_behaves_like 'a module that requires auth' - # end - - # context 'with a public project', visibility: 'public' do - # let(:module_name) { base } - - # it_behaves_like 'a module that does not require auth' - # end - context 'for the root module' do let(:module_name) { base } @@ -454,56 +317,222 @@ end end - before do - project.add_developer(user) - stub_licensed_features(packages: true) + context 'with an invalid module directive' do + let_it_be(:project) { create :project_empty_repo, :public, creator: user } + let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } - modules + # rubocop: disable Layout/IndentationWidth + let_it_be(:modules) do + create_file('a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") + create_version(1, 0, 0, create_file('go.mod', "module not/a/real/module\n")) + create_file('v2/a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") + create_version(2, 0, 0, create_file('v2/go.mod', "module #{base}\n")) + + project.repository.head_commit + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + context 'with a completely wrong directive for v1' do + let(:module_name) { base } + + it 'returns nothing' do + get_resource(user) + + expect_module_version_list + end + end + + context 'with a directive omitting the suffix for v2' do + let(:module_name) { "#{base}/v2" } + + it 'returns nothing' do + get_resource(user) + + expect_module_version_list + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with a completely wrong directive for v1' do + let(:module_name) { base } + let(:resource) { "v1.0.0.info" } + + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with a directive omitting the suffix for v2' do + let(:module_name) { "#{base}/v2" } + let(:resource) { "v2.0.0.info" } + + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end - shared_context 'has a private project', visibility: 'private' do - before do - project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + context 'with a case sensitive project and versions' do + let_it_be(:project) { create :project_empty_repo, :public, creator: user, path: 'MyGoLib' } + let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } + let_it_be(:base_encoded) { base.gsub(/[A-Z]/) { |s| "!#{s.downcase}"} } + + let_it_be(:modules) do + create_file('README.md', 'Hi', commit_message: 'Add README.md') + create_version(1, 0, 1, create_module, prerelease: 'prerelease') + create_version(1, 0, 1, create_package('pkg'), prerelease: 'Prerelease') + + project.repository.head_commit + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + context 'with a case encoded path' do + let(:module_name) { base_encoded } + + it 'returns the tags' do + get_resource(user) + + expect_module_version_list('v1.0.1-prerelease', 'v1.0.1-Prerelease') + end + end + + context 'without a case encoded path' do + let(:module_name) { base.downcase } + + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with a case encoded path' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-!prerelease.info" } + + it 'returns the uppercase tag' do + get_resource(user) + + expect_module_version_info('v1.0.1-Prerelease') + end + end + + context 'without a case encoded path' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-prerelease.info" } + + it 'returns the lowercase tag' do + get_resource(user) + + expect_module_version_info('v1.0.1-prerelease') + end + end end end - shared_context 'has a public project', visibility: 'public' do + context 'with a private project' do + let(:module_name) { base } + before do - project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + it 'returns ok with an oauth token' do + get_resource(oauth_access_token: oauth) + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns ok with a job token' do + get_resource(oauth_access_token: job) + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns ok with a personal access token' do + get_resource(personal_access_token: pa_token) + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns unauthorized with no authentication' do + get_resource + expect(response).to have_gitlab_http_status(:unauthorized) + end end end - shared_examples 'a module that requires auth' do - it 'returns ok with oauth token' do - get_resource(access_token: oauth.token) - expect(response).to have_gitlab_http_status(:ok) + context 'with a public project' do + let(:module_name) { base } + + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end - it 'returns ok with job token' do - get_resource(job_token: job.token) - expect(response).to have_gitlab_http_status(:ok) + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + it 'returns ok with no authentication' do + get_resource + expect(response).to have_gitlab_http_status(:ok) + end end + end - it 'returns ok with personal access token' do - get_resource(personal_access_token: pa_token) - expect(response).to have_gitlab_http_status(:ok) + context 'with a non-existent project' do + def get_resource(user = nil, **params) + get api("/projects/not%2fa%2fproject/packages/go/#{base}/@v/list", user, params) end - it 'returns not found with no authentication' do - get_resource - expect(response).to have_gitlab_http_status(:not_found) + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + it 'returns not found with a user' do + get_resource(user) + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with an oauth token' do + get_resource(oauth_access_token: oauth) + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with a job token' do + get_resource(oauth_access_token: job) + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with a personal access token' do + get_resource(personal_access_token: pa_token) + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns unauthorized with no authentication' do + get_resource + expect(response).to have_gitlab_http_status(:unauthorized) + end end end - shared_examples 'a module that does not require auth' do - it 'returns ok with no authentication' do - get_resource - expect(response).to have_gitlab_http_status(:ok) - end + before do + project.add_developer(user) + stub_licensed_features(packages: true) + + modules end def get_resource(user = nil, **params) - get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user), params: params + get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params) end def create_file(path, content, commit_message: 'Add file') -- GitLab From e1009191a56e2ad3e779c59f8d2656ae64cb1303 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Thu, 9 Apr 2020 01:11:20 -0500 Subject: [PATCH 09/19] Refactor Go module proxy to add finders --- ee/app/finders/packages/go/module_finder.rb | 36 ++++++ ee/app/finders/packages/go/version_finder.rb | 80 +++++++++++++ ee/app/models/packages/go_module.rb | 40 +++---- ee/app/models/packages/go_module_version.rb | 105 ++---------------- ee/lib/api/go_proxy.rb | 14 ++- .../api/helpers/packages/go/module_helpers.rb | 50 +++++++++ 6 files changed, 201 insertions(+), 124 deletions(-) create mode 100644 ee/app/finders/packages/go/module_finder.rb create mode 100644 ee/app/finders/packages/go/version_finder.rb create mode 100644 ee/lib/api/helpers/packages/go/module_helpers.rb diff --git a/ee/app/finders/packages/go/module_finder.rb b/ee/app/finders/packages/go/module_finder.rb new file mode 100644 index 00000000000000..f032de77809f05 --- /dev/null +++ b/ee/app/finders/packages/go/module_finder.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Packages + module Go + class ModuleFinder + include ::API::Helpers::Packages::Go::ModuleHelpers + + attr_reader :project, :module_name + + def initialize(project, module_name) + module_name = Pathname.new(module_name).cleanpath.to_s + + @project = project + @module_name = module_name + end + + def execute + return if @module_name.blank? + + if @module_name == package_base + Packages::GoModule.new(@project, @module_name, '') + elsif @module_name.start_with?(package_base + '/') + Packages::GoModule.new(@project, @module_name, @module_name[(package_base.length + 1)..]) + else + nil + end + end + + private + + def package_base + @package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1] + end + end + end +end diff --git a/ee/app/finders/packages/go/version_finder.rb b/ee/app/finders/packages/go/version_finder.rb new file mode 100644 index 00000000000000..ef7d24e125782c --- /dev/null +++ b/ee/app/finders/packages/go/version_finder.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Packages + module Go + class VersionFinder + include ::API::Helpers::Packages::Go::ModuleHelpers + + attr_reader :mod + + def initialize(mod) + @mod = mod + end + + def execute + @mod.project.repository.tags + .filter { |tag| semver? tag } + .map { |tag| find_ref tag } + .filter { |ver| ver.valid? } + end + + def find(target) + case target + when String + unless pseudo_version? target + return mod.versions.filter { |v| v.name == target }.first + end + + begin + find_pseudo_version target + rescue ArgumentError + nil + end + + when Gitlab::Git::Ref + find_ref target + + when ::Commit, Gitlab::Git::Commit + find_commit target + + else + raise ArgumentError.new 'not a valid target' + end + end + + private + + def find_ref(ref) + commit = ref.dereferenced_target + Packages::GoModuleVersion.new(@mod, :ref, commit, ref: ref, semver: parse_semver(ref.name)) + end + + def find_commit(commit) + Packages::GoModuleVersion.new(@mod, :commit, commit) + end + + def find_pseudo_version(str) + semver = parse_semver(str) + raise ArgumentError.new 'target is not a pseudo-version' unless semver && PSEUDO_VERSION_REGEX.match?(str) + + # valid pseudo-versions are + # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X + # vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre + # vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z + + # go discards the timestamp when resolving pseudo-versions, so we will do the same + + timestamp, sha = semver.prerelease.split('-').last 2 + timestamp = timestamp.split('.').last + commit = @mod.project.repository.commit_by(oid: sha) + + # these errors are copied from proxy.golang.org's responses + raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless commit + raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12 + raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp + + Packages::GoModuleVersion.new(@mod, :pseudo, commit, name: str, semver: semver) + end + end + end +end diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb index 4f9961bcae9b2a..9be705d7a39f06 100644 --- a/ee/app/models/packages/go_module.rb +++ b/ee/app/models/packages/go_module.rb @@ -3,42 +3,32 @@ class Packages::GoModule attr_reader :project, :name, :path - def initialize(project, name) + def initialize(project, name, path) @project = project @name = name - - @path = - if @name == package_base - '' - elsif @name.start_with?(package_base + '/') - @name[(package_base.length + 1)..] - else - nil - end + @path = path end def versions - @versions ||= @project.repository.tags - .filter { |tag| ::Packages::GoModuleVersion.semver? tag } - .map { |tag| ::Packages::GoModuleVersion.new self, tag } - .filter { |ver| ver.valid? } + @versions ||= Packages::Go::VersionFinder.new(self).execute end def find_version(name) - if ::Packages::GoModuleVersion.pseudo_version? name - begin - ::Packages::GoModuleVersion.new self, name - rescue ArgumentError - nil - end + Packages::Go::VersionFinder.new(self).find(name) + end + + def path_valid?(major) + m = /\/v(\d+)$/i.match(@name) + + case major + when 0, 1 + m.nil? else - versions.filter { |ver| ver.name == name }.first + !m.nil? && m[1].to_i == major end end - private - - def package_base - @package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1] + def gomod_valid?(gomod) + gomod&.split("\n", 2)&.first == "module #{@name}" end end diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index f65041c1af837f..92b7cccca47c65 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true class Packages::GoModuleVersion - SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze - SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze - PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze - VERSION_SUFFIX_REGEX = /\/v([1-9]\d*)$/i.freeze + include ::API::Helpers::Packages::Go::ModuleHelpers attr_reader :mod, :type, :ref, :commit @@ -14,57 +11,13 @@ class Packages::GoModuleVersion delegate :prerelease, to: :@semver, allow_nil: true delegate :build, to: :@semver, allow_nil: true - def self.semver?(tag) - return false if tag.dereferenced_target.nil? - - SEMVER_TAG_REGEX.match?(tag.name) - end - - def self.pseudo_version?(str) - SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str) - end - - def initialize(mod, target) + def initialize(mod, type, commit, name: nil, semver: nil, ref: nil) @mod = mod - - case target - when String - m = SEMVER_TAG_REGEX.match(target) - raise ArgumentError.new 'target is not a pseudo-version' unless m && PSEUDO_VERSION_REGEX.match?(target) - - # valid pseudo-versions are - # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X - # vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre - # vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z - - # go discards the timestamp when resolving pseudo-versions, so we will do the same - - @type = :pseudo - @name = target - @semver = semver_match_to_hash m - - timestamp, sha = prerelease.split('-').last 2 - timestamp = timestamp.split('.').last - @commit = mod.project.repository.commit_by(oid: sha) - - # these errors are copied from proxy.golang.org's responses - raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless @commit - raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12 - raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless @commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp - - when Gitlab::Git::Ref - @type = :ref - @ref = target - @commit = target.dereferenced_target - @semver = semver_match_to_hash SEMVER_TAG_REGEX.match(target.name) - - when ::Commit, Gitlab::Git::Commit - @type = :commit - @commit = target - - else - raise ArgumentError.new 'not a valid target' - end + @type = type + @commit = commit + @name = name if name + @semver = semver if semver + @ref = ref if ref end def name @@ -72,36 +25,11 @@ def name end def gomod - @gomod ||= @mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data - end - - def valid? - valid_path? && valid_module? - end - - def valid_path? - m = VERSION_SUFFIX_REGEX.match(@mod.name) - - case major - when 0, 1 - m.nil? - else - !m.nil? && m[1].to_i == major - end - end - - def valid_module? - return false unless gomod - - gomod.split("\n", 2).first == "module #{@mod.name}" - end - - def pseudo? - @type == :pseudo + @gomod ||= blob_at(@mod.path + '/go.mod') end def files - return @files unless @files.nil? + return @files if defined?(@files) sha = @commit.sha tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } @@ -110,19 +38,10 @@ def files end def blob_at(path) - @mod.project.repository.blob_at(@commit.sha, path).data + @mod.project.repository.blob_at(@commit.sha, path)&.data end - private - - def semver_match_to_hash(match) - return unless match - - OpenStruct.new( - major: match[1].to_i, - minor: match[2].to_i, - patch: match[3].to_i, - prerelease: match[4], - build: match[5]) + def valid? + @mod.path_valid?(major) && @mod.gomod_valid?(gomod) end end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index 16f8423e246ea4..7c30610a9d4125 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -2,8 +2,11 @@ module API class GoProxy < Grape::API helpers ::API::Helpers::PackagesHelpers + helpers ::API::Helpers::Packages::Go::ModuleHelpers + # basic semver, except case encoded (A => !a) MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze + MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze before { require_packages_enabled! } @@ -15,25 +18,24 @@ def case_decode(str) def find_module module_name = case_decode params[:module_name] - bad_request!('Module Name') if module_name.blank? - mod = ::Packages::GoModule.new user_project, module_name - not_found! if mod.path.nil? + mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute + + not_found! unless mod mod end def find_version - mod = find_module + module_version = case_decode params[:module_version] + ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version) - ver = mod.find_version case_decode params[:module_version] not_found! unless ver&.valid? ver end - # override :find_project! def find_project!(id) project = find_project(id) diff --git a/ee/lib/api/helpers/packages/go/module_helpers.rb b/ee/lib/api/helpers/packages/go/module_helpers.rb new file mode 100644 index 00000000000000..d1a4c315ecea7b --- /dev/null +++ b/ee/lib/api/helpers/packages/go/module_helpers.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module API + module Helpers + module Packages + module Go + module ModuleHelpers + # basic semver regex + SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze + + # basic semver, but bounded (^expr$) + SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze + + # semver, but the prerelease component follows a specific format + PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze + + def case_encode(str) + str.gsub(/A-Z/) { |s| "!#{s.downcase}"} + end + + def case_decode(str) + str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase } + end + + def semver?(tag) + return false if tag.dereferenced_target.nil? + + SEMVER_TAG_REGEX.match?(tag.name) + end + + def pseudo_version?(str) + SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str) + end + + def parse_semver(str) + m = SEMVER_TAG_REGEX.match(str) + return unless m + + OpenStruct.new( + major: m[1].to_i, + minor: m[2].to_i, + patch: m[3].to_i, + prerelease: m[4], + build: m[5]) + end + end + end + end + end +end -- GitLab From 1a0ec49629fd23542e9e30011cbacbafe26055a9 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Tue, 7 Apr 2020 22:21:19 +0000 Subject: [PATCH 10/19] Improve tests and apply suggestions for Go proxy See !27746 - Fix rubocop disable comments - Move `before` block to before tests - Correct rubocop alerts due to new rules - Use shared examples to clarify Go proxy spec - Enable HTTP Basic authentication for Go proxy + Support both HTTP basic and normal token header/query var + Remove custom `find_project!` helper and use basic auth helpers - Validate GoModuleVersion type attribute - Implement testing factories - Implement specs for untested new classes - Add a Settings helper for Go URLs --- config/settings.rb | 6 + ee/app/finders/packages/go/module_finder.rb | 22 +- ee/app/finders/packages/go/version_finder.rb | 10 +- ee/app/models/packages/go_module_version.rb | 13 + ee/app/models/packages/sem_ver.rb | 57 ++ ee/lib/api/go_proxy.rb | 42 +- .../api/helpers/packages/go/module_helpers.rb | 17 +- ee/spec/factories/go_module_commits.rb | 113 ++++ ee/spec/factories/go_module_versions.rb | 50 ++ ee/spec/factories/go_modules.rb | 12 + .../finders/packages/go/module_finder_spec.rb | 42 ++ .../packages/go/version_finder_spec.rb | 139 +++++ ee/spec/models/packages/go_module_spec.rb | 55 ++ .../models/packages/go_module_version_spec.rb | 70 +++ ee/spec/models/packages/sem_ver_spec.rb | 42 ++ ee/spec/requests/api/go_proxy_spec.rb | 568 ++++++------------ 16 files changed, 826 insertions(+), 432 deletions(-) create mode 100644 ee/app/models/packages/sem_ver.rb create mode 100644 ee/spec/factories/go_module_commits.rb create mode 100644 ee/spec/factories/go_module_versions.rb create mode 100644 ee/spec/factories/go_modules.rb create mode 100644 ee/spec/finders/packages/go/module_finder_spec.rb create mode 100644 ee/spec/finders/packages/go/version_finder_spec.rb create mode 100644 ee/spec/models/packages/go_module_spec.rb create mode 100644 ee/spec/models/packages/go_module_version_spec.rb create mode 100644 ee/spec/models/packages/sem_ver_spec.rb diff --git a/config/settings.rb b/config/settings.rb index 144a068ef2af76..99f1b85202e39d 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -66,6 +66,12 @@ def build_gitlab_url (base_url(gitlab) + [gitlab.relative_url_root]).join('') end + def build_gitlab_go_url + # "Go package paths are not URLs, and do not include port numbers" + # https://github.com/golang/go/issues/38213#issuecomment-607851460 + "#{gitlab.host}#{gitlab.relative_url_root}" + end + def kerberos_protocol kerberos.https ? "https" : "http" end diff --git a/ee/app/finders/packages/go/module_finder.rb b/ee/app/finders/packages/go/module_finder.rb index f032de77809f05..7c4e382c24aa18 100644 --- a/ee/app/finders/packages/go/module_finder.rb +++ b/ee/app/finders/packages/go/module_finder.rb @@ -5,6 +5,8 @@ module Go class ModuleFinder include ::API::Helpers::Packages::Go::ModuleHelpers + GITLAB_GO_URL = (Settings.build_gitlab_go_url + '/').freeze + attr_reader :project, :module_name def initialize(project, module_name) @@ -14,23 +16,17 @@ def initialize(project, module_name) @module_name = module_name end + # rubocop: disable CodeReuse/ActiveRecord def execute - return if @module_name.blank? - - if @module_name == package_base - Packages::GoModule.new(@project, @module_name, '') - elsif @module_name.start_with?(package_base + '/') - Packages::GoModule.new(@project, @module_name, @module_name[(package_base.length + 1)..]) - else - nil - end - end + return if @module_name.blank? || !@module_name.start_with?(GITLAB_GO_URL) - private + module_path = @module_name[GITLAB_GO_URL.length..].split('/') + project_path = project.full_path.split('/') + return unless module_path.take(project_path.length) == project_path - def package_base - @package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1] + Packages::GoModule.new(@project, @module_name, module_path.drop(project_path.length).join('/')) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/ee/app/finders/packages/go/version_finder.rb b/ee/app/finders/packages/go/version_finder.rb index ef7d24e125782c..7504554e253c7c 100644 --- a/ee/app/finders/packages/go/version_finder.rb +++ b/ee/app/finders/packages/go/version_finder.rb @@ -21,14 +21,10 @@ def execute def find(target) case target when String - unless pseudo_version? target - return mod.versions.filter { |v| v.name == target }.first - end - - begin + if pseudo_version? target find_pseudo_version target - rescue ArgumentError - nil + else + mod.versions.find { |v| v.name == target } end when Gitlab::Git::Ref diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index 92b7cccca47c65..3575d1361ceb73 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -3,6 +3,8 @@ class Packages::GoModuleVersion include ::API::Helpers::Packages::Go::ModuleHelpers + VALID_TYPES = %i[ref commit pseudo].freeze + attr_reader :mod, :type, :ref, :commit delegate :major, to: :@semver, allow_nil: true @@ -12,6 +14,17 @@ class Packages::GoModuleVersion delegate :build, to: :@semver, allow_nil: true def initialize(mod, type, commit, name: nil, semver: nil, ref: nil) + raise ArgumentError.new("invalid type '#{type}'") unless VALID_TYPES.include? type + raise ArgumentError.new("mod is required") unless mod + raise ArgumentError.new("commit is required") unless commit + + if type == :ref + raise ArgumentError.new("ref is required") unless ref + elsif type == :pseudo + raise ArgumentError.new("name is required") unless name + raise ArgumentError.new("semver is required") unless semver + end + @mod = mod @type = type @commit = commit diff --git a/ee/app/models/packages/sem_ver.rb b/ee/app/models/packages/sem_ver.rb new file mode 100644 index 00000000000000..760590f3c5a175 --- /dev/null +++ b/ee/app/models/packages/sem_ver.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class Packages::SemVer + # basic semver, but bounded (^expr$) + PATTERN = /\A(v?)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?\z/i.freeze + + attr_accessor :major, :minor, :patch, :prerelease, :build + + def initialize(major = 0, minor = 0, patch = 0, prerelease = nil, build = nil, prefixed: false) + @major = major + @minor = minor + @patch = patch + @prerelease = prerelease + @build = build + @prefixed = prefixed + end + + def prefixed? + @prefixed + end + + def ==(other) + self.class == other.class && + self.major == other.major && + self.minor == other.minor && + self.patch == other.patch && + self.prerelease == other.prerelease && + self.build == other.build + end + + def to_s + s = "#{prefixed? ? 'v' : ''}#{major || 0}.#{minor || 0}.#{patch || 0}" + s += "-#{prerelease}" if prerelease + s += "+#{build}" if build + + s + end + + def self.match(str, prefixed: false) + m = PATTERN.match(str) + return unless m + return if prefixed == m[1].empty? + + m + end + + def self.match?(str, prefixed: false) + !match(str, prefixed: prefixed).nil? + end + + def self.parse(str, prefixed: false) + m = match str, prefixed: prefixed + return unless m + + new(m[2].to_i, m[3].to_i, m[4].to_i, m[5], m[6], prefixed: prefixed) + end +end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index 7c30610a9d4125..fe045204c1da25 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module API class GoProxy < Grape::API - helpers ::API::Helpers::PackagesHelpers + helpers ::API::Helpers::PackagesManagerClientsHelpers + helpers ::API::Helpers::Packages::BasicAuthHelpers helpers ::API::Helpers::Packages::Go::ModuleHelpers # basic semver, except case encoded (A => !a) @@ -12,15 +13,27 @@ class GoProxy < Grape::API before { require_packages_enabled! } helpers do - def case_decode(str) - str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase } + # support personal access tokens for HTTP Basic in addition to the usual methods + def find_personal_access_token + pa = find_personal_access_token_from_http_basic_auth + return pa if pa + + # copied from Gitlab::Auth::AuthFinders + token = + current_request.params[::Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_PARAM].presence || + current_request.env[::Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER].presence || + parsed_oauth_token + return unless token + + # Expiration, revocation and scopes are verified in `validate_access_token!` + PersonalAccessToken.find_by_token(token) || raise(::Gitlab::Auth::UnauthorizedError) end def find_module module_name = case_decode params[:module_name] bad_request!('Module Name') if module_name.blank? - mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute + mod = ::Packages::Go::ModuleFinder.new(authorized_user_project, module_name).execute not_found! unless mod @@ -29,25 +42,14 @@ def find_module def find_version module_version = case_decode params[:module_version] - ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version) + ver = find_module.find_version(module_version) not_found! unless ver&.valid? ver - end - - def find_project!(id) - project = find_project(id) - ability = job_token_authentication? ? :build_read_project : :read_project - - if can?(current_user, ability, project) - project - elsif current_user.nil? - unauthorized! - else - not_found!('Project') - end + rescue ArgumentError + not_found! end end @@ -58,8 +60,8 @@ def find_project!(id) route_setting :authentication, job_token_allowed: true resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before do - authorize_read_package! - authorize_packages_feature! + authorize_read_package!(authorized_user_project) + authorize_packages_feature!(authorized_user_project) end namespace ':id/packages/go/*module_name/@v' do diff --git a/ee/lib/api/helpers/packages/go/module_helpers.rb b/ee/lib/api/helpers/packages/go/module_helpers.rb index d1a4c315ecea7b..375c44460cf136 100644 --- a/ee/lib/api/helpers/packages/go/module_helpers.rb +++ b/ee/lib/api/helpers/packages/go/module_helpers.rb @@ -8,9 +8,6 @@ module ModuleHelpers # basic semver regex SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze - # basic semver, but bounded (^expr$) - SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze - # semver, but the prerelease component follows a specific format PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze @@ -25,23 +22,15 @@ def case_decode(str) def semver?(tag) return false if tag.dereferenced_target.nil? - SEMVER_TAG_REGEX.match?(tag.name) + ::Packages::SemVer.match?(tag.name, prefixed: true) end def pseudo_version?(str) - SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str) + ::Packages::SemVer.match?(str, prefixed: true) && PSEUDO_VERSION_REGEX.match?(str) end def parse_semver(str) - m = SEMVER_TAG_REGEX.match(str) - return unless m - - OpenStruct.new( - major: m[1].to_i, - minor: m[2].to_i, - patch: m[3].to_i, - prerelease: m[4], - build: m[5]) + ::Packages::SemVer.parse(str, prefixed: true) end end end diff --git a/ee/spec/factories/go_module_commits.rb b/ee/spec/factories/go_module_commits.rb new file mode 100644 index 00000000000000..4dc3df88252bb9 --- /dev/null +++ b/ee/spec/factories/go_module_commits.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +def get_result(op, ret) + raise "#{op} failed: #{ret}" unless ret[:status] == :success + + ret[:result] +end + +FactoryBot.define do + factory :go_module_commit, class: 'Commit' do + skip_create + + transient do + project { raise ArgumentError.new("project is required") } + service { raise ArgumentError.new("this factory cannot be used without specifying a trait") } + tag { nil } + tag_message { nil } + + commit do + r = service.execute + + raise "operation failed: #{r}" unless r[:status] == :success + + commit = project.repository.commit_by(oid: r[:result]) + + if tag + r = Tags::CreateService.new(project, project.owner).execute(tag, commit.sha, tag_message) + + raise "operation failed: #{r}" unless r[:status] == :success + end + + commit + end + end + + initialize_with do + commit + end + + trait :files do + transient do + files { raise ArgumentError.new("files is required") } + message { 'Add files' } + end + + service do + Files::MultiService.new( + project, + project.owner, + commit_message: message, + start_branch: project.repository.root_ref || 'master', + branch_name: project.repository.root_ref || 'master', + actions: files.map do |path, content| + { action: :create, file_path: path, content: content } + end + ) + end + end + + trait :package do + transient do + path { raise ArgumentError.new("path is required") } + message { 'Add package' } + end + + service do + Files::MultiService.new( + project, + project.owner, + commit_message: message, + start_branch: project.repository.root_ref || 'master', + branch_name: project.repository.root_ref || 'master', + actions: [ + { action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" } + ] + ) + end + end + + trait :module do + transient do + name { nil } + message { 'Add module' } + end + + service do + port = ::Gitlab.config.gitlab.port + host = ::Gitlab.config.gitlab.host + domain = case port when 80, 443 then host else "#{host}:#{port}" end + + url = "#{domain}/#{project.path_with_namespace}" + if name.nil? + path = '' + else + url += '/' + name + path = name + '/' + end + + Files::MultiService.new( + project, + project.owner, + commit_message: message, + start_branch: project.repository.root_ref || 'master', + branch_name: project.repository.root_ref || 'master', + actions: [ + { action: :create, file_path: path + 'go.mod', content: "module #{url}\n" }, + { action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" } + ] + ) + end + end + end +end diff --git a/ee/spec/factories/go_module_versions.rb b/ee/spec/factories/go_module_versions.rb new file mode 100644 index 00000000000000..d87c9e31f82e3b --- /dev/null +++ b/ee/spec/factories/go_module_versions.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +def get_result(op, ret) + raise "#{op} failed: #{ret}" unless ret[:status] == :success + + ret[:result] +end + +FactoryBot.define do + factory :go_module_version, class: 'Packages::GoModuleVersion' do + skip_create + + initialize_with do + p = attributes[:params] + s = Packages::SemVer.parse(p.semver, prefixed: true) + + raise ArgumentError.new("invalid sematic version: '#{p.semver}''") if !s && p.semver + + new(p.mod, p.type, p.commit, name: p.name, semver: s, ref: p.ref) + end + + mod { go_module } + type { :commit } + commit { raise ArgumentError.new("commit is required") } + name { nil } + semver { nil } + ref { nil } + + params { OpenStruct.new(mod: mod, type: type, commit: commit, name: name, semver: semver, ref: ref) } + + trait :tagged do + name { raise ArgumentError.new("name is required") } + ref { mod.project.repository.find_tag(name) } + commit { ref.dereferenced_target } + + params { OpenStruct.new(mod: mod, type: :ref, commit: commit, semver: name, ref: ref) } + end + + trait :pseudo do + transient do + prefix { raise ArgumentError.new("prefix is required") } + end + + type { :pseudo } + name { "#{prefix}#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" } + + params { OpenStruct.new(mod: mod, type: :pseudo, commit: commit, name: name, semver: name) } + end + end +end diff --git a/ee/spec/factories/go_modules.rb b/ee/spec/factories/go_modules.rb new file mode 100644 index 00000000000000..5f5fbacc0e0c5c --- /dev/null +++ b/ee/spec/factories/go_modules.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :go_module, class: 'Packages::GoModule' do + initialize_with { new(attributes[:project], attributes[:name], attributes[:path]) } + skip_create + + project + path { '' } + name { "#{Settings.build_gitlab_go_url}/#{project.full_path}#{path.empty? ? '' : path}" } + end +end diff --git a/ee/spec/finders/packages/go/module_finder_spec.rb b/ee/spec/finders/packages/go/module_finder_spec.rb new file mode 100644 index 00000000000000..914812ebed41ff --- /dev/null +++ b/ee/spec/finders/packages/go/module_finder_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Packages::Go::ModuleFinder do + let_it_be(:project) { create :project } + let_it_be(:other_project) { create :project } + + describe '#execute' do + context 'with module name equal to project name' do + let(:finder) { described_class.new(project, base_url(project)) } + + it 'returns a module with empty path' do + mod = finder.execute + expect(mod).not_to be_nil + expect(mod.path).to eq('') + end + end + + context 'with module name starting with project name and slash' do + let(:finder) { described_class.new(project, base_url(project) + '/mod') } + + it 'returns a module with non-empty path' do + mod = finder.execute + expect(mod).not_to be_nil + expect(mod.path).to eq('mod') + end + end + + context 'with a module name not equal to and not starting with project name' do + let(:finder) { described_class.new(project, base_url(other_project)) } + + it 'returns nil' do + expect(finder.execute).to be_nil + end + end + end + + def base_url(project) + "#{Settings.build_gitlab_go_url}/#{project.full_path}" + end +end diff --git a/ee/spec/finders/packages/go/version_finder_spec.rb b/ee/spec/finders/packages/go/version_finder_spec.rb new file mode 100644 index 00000000000000..1459e693345b95 --- /dev/null +++ b/ee/spec/finders/packages/go/version_finder_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Packages::Go::VersionFinder do + let_it_be(:user) { create :user } + let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } + + let(:finder) { described_class.new mod } + + before :all do + create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } + create :go_module_commit, :module, project: project, tag: 'v1.0.1' + create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' + create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' + create :go_module_commit, :files, project: project, tag: 'c1', files: { 'y.go' => "package a\n" } + create :go_module_commit, :module, project: project, tag: 'c2', name: 'v2' + create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } + end + + shared_examples '#execute' do |*expected| + it "returns #{expected.empty? ? 'nothing' : expected.join(', ')}" do + actual = finder.execute.map { |x| x.name } + expect(actual.to_set).to eq(expected.to_set) + end + end + + shared_examples '#find with an invalid argument' do |message| + it "raises an argument exception: #{message}" do + expect { finder.find(target) }.to raise_error(ArgumentError, message) + end + end + + describe '#execute' do + context 'for the root module' do + let(:mod) { create :go_module, project: project } + + it_behaves_like '#execute', 'v1.0.1', 'v1.0.2', 'v1.0.3' + end + + context 'for the package' do + let(:mod) { create :go_module, project: project, path: '/pkg' } + + it_behaves_like '#execute' + end + + context 'for the submodule' do + let(:mod) { create :go_module, project: project, path: '/mod' } + + it_behaves_like '#execute', 'v1.0.3' + end + + context 'for the root module v2' do + let(:mod) { create :go_module, project: project, path: '/v2' } + + it_behaves_like '#execute', 'v2.0.0' + end + end + + describe '#find' do + let(:mod) { create :go_module, project: project } + + context 'with a ref' do + it 'returns a ref version' do + ref = project.repository.find_branch 'master' + v = finder.find(ref) + expect(v.type).to eq(:ref) + expect(v.ref).to eq(ref) + end + end + + context 'with a semver tag' do + it 'returns a version with a semver' do + v = finder.find(project.repository.find_tag('v1.0.0')) + expect(v.major).to eq(1) + expect(v.minor).to eq(0) + expect(v.patch).to eq(0) + expect(v.prerelease).to be_nil + expect(v.build).to be_nil + end + end + + context 'with a semver tag string' do + it 'returns a version with a semver' do + v = finder.find('v1.0.1') + expect(v.major).to eq(1) + expect(v.minor).to eq(0) + expect(v.patch).to eq(1) + expect(v.prerelease).to be_nil + expect(v.build).to be_nil + end + end + + context 'with a commit' do + it 'retruns a commit version' do + v = finder.find(project.repository.head_commit) + expect(v.type).to eq(:commit) + end + end + + context 'with a pseudo-version' do + it 'returns a pseudo version' do + commit = project.repository.head_commit + pseudo = "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" + v = finder.find(pseudo) + expect(v.type).to eq(:pseudo) + expect(v.commit).to eq(commit) + expect(v.name).to eq(pseudo) + end + end + + context 'with a string that is not a semantic version' do + it 'returns nil' do + expect(finder.find('not-a-semver')).to be_nil + end + end + + context 'with a pseudo-version that does not reference a commit' do + it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: unknown commit' do + let(:commit) { project.repository.head_commit } + let(:target) { "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{'0' * 12}" } + end + end + + context 'with a pseudo-version with a short sha' do + it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: revision is shorter than canonical' do + let(:commit) { project.repository.head_commit } + let(:target) { "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..10]}" } + end + end + + context 'with a pseudo-version with an invalid timestamp' do + it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: does not match version-control timestamp' do + let(:commit) { project.repository.head_commit } + let(:target) { "v0.0.0-#{'0' * 14}-#{commit.sha[0..11]}" } + end + end + end +end diff --git a/ee/spec/models/packages/go_module_spec.rb b/ee/spec/models/packages/go_module_spec.rb new file mode 100644 index 00000000000000..823b41401b99d5 --- /dev/null +++ b/ee/spec/models/packages/go_module_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Packages::GoModule, type: :model do + describe '#path_valid?' do + context 'with root path' do + let_it_be(:package) { create(:go_module) } + + context 'with major version 0' do + it('returns true') { expect(package.path_valid?(0)).to eq(true) } + end + + context 'with major version 1' do + it('returns true') { expect(package.path_valid?(1)).to eq(true) } + end + + context 'with major version 2' do + it('returns false') { expect(package.path_valid?(2)).to eq(false) } + end + end + + context 'with path ./v2' do + let_it_be(:package) { create(:go_module, path: '/v2') } + + context 'with major version 0' do + it('returns false') { expect(package.path_valid?(0)).to eq(false) } + end + + context 'with major version 1' do + it('returns false') { expect(package.path_valid?(1)).to eq(false) } + end + + context 'with major version 2' do + it('returns true') { expect(package.path_valid?(2)).to eq(true) } + end + end + end + + describe '#gomod_valid?' do + let_it_be(:package) { create(:go_module) } + + context 'with good gomod' do + it('returns true') { expect(package.gomod_valid?("module #{package.name}")).to eq(true) } + end + + context 'with bad gomod' do + it('returns false') { expect(package.gomod_valid?("module #{package.name}/v2")).to eq(false) } + end + + context 'with empty gomod' do + it('returns false') { expect(package.gomod_valid?("")).to eq(false) } + end + end +end diff --git a/ee/spec/models/packages/go_module_version_spec.rb b/ee/spec/models/packages/go_module_version_spec.rb new file mode 100644 index 00000000000000..2db15a702ab105 --- /dev/null +++ b/ee/spec/models/packages/go_module_version_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Packages::GoModuleVersion, type: :model do + let_it_be(:user) { create :user } + let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } + let_it_be(:mod) { create :go_module, project: project } + + before :all do + create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } + create :go_module_commit, :module, project: project, tag: 'v1.0.1' + create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' + create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' + create :go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } + create :go_module_commit, :module, project: project, name: 'v2' + create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } + end + + describe '#name' do + context 'with ref and name specified' do + let_it_be(:version) { create :go_module_version, mod: mod, name: 'foobar', commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } + it('returns that name') { expect(version.name).to eq('foobar') } + end + + context 'with ref specified and name unspecified' do + let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } + it('returns the name of the ref') { expect(version.name).to eq('v1.0.0') } + end + + context 'with ref and name unspecified' do + let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit } + it('returns nil') { expect(version.name).to eq(nil) } + end + end + + describe '#gomod' do + context 'with go.mod missing' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.0' } + it('returns nil') { expect(version.gomod).to eq(nil) } + end + + context 'with go.mod present' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' } + it('returns the contents of go.mod') { expect(version.gomod).to eq("module #{mod.name}\n") } + end + end + + describe '#files' do + context 'with a root module' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } + it('returns all the files') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) } + end + end + + context 'with a root module and a submodule' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it('returns files excluding the submodule') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) } + end + + context 'with the submodule\'s path' do + let_it_be(:mod) { create :go_module, project: project, path: 'mod' } + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it('returns the submodule\'s files') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['mod/go.mod', 'mod/a.go']) } + end + end + end +end diff --git a/ee/spec/models/packages/sem_ver_spec.rb b/ee/spec/models/packages/sem_ver_spec.rb new file mode 100644 index 00000000000000..3fedc39fb0348a --- /dev/null +++ b/ee/spec/models/packages/sem_ver_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Packages::SemVer, type: :model do + shared_examples '#parse with a valid semver' do |str, major, minor, patch, prerelease, build| + context "with #{str}" do + it "returns #{described_class.new(major, minor, patch, prerelease, build, prefixed: true)} with prefix" do + expected = described_class.new(major, minor, patch, prerelease, build, prefixed: true) + expect(described_class.parse('v' + str, prefixed: true)).to eq(expected) + end + + it "returns #{described_class.new(major, minor, patch, prerelease, build)} without prefix" do + expected = described_class.new(major, minor, patch, prerelease, build) + expect(described_class.parse(str)).to eq(expected) + end + end + end + + shared_examples '#parse with an invalid semver' do |str| + context "with #{str}" do + it 'returns nil with prefix' do + expect(described_class.parse('v' + str, prefixed: true)).to be_nil + end + + it 'returns nil without prefix' do + expect(described_class.parse(str)).to be_nil + end + end + end + + describe '#parse' do + it_behaves_like '#parse with a valid semver', '1.0.0', 1, 0, 0, nil, nil + it_behaves_like '#parse with a valid semver', '1.0.0-pre', 1, 0, 0, 'pre', nil + it_behaves_like '#parse with a valid semver', '1.0.0+build', 1, 0, 0, nil, 'build' + it_behaves_like '#parse with a valid semver', '1.0.0-pre+build', 1, 0, 0, 'pre', 'build' + it_behaves_like '#parse with an invalid semver', '01.0.0' + it_behaves_like '#parse with an invalid semver', '0.01.0' + it_behaves_like '#parse with an invalid semver', '0.0.01' + it_behaves_like '#parse with an invalid semver', '1.0.0asdf' + end +end diff --git a/ee/spec/requests/api/go_proxy_spec.rb b/ee/spec/requests/api/go_proxy_spec.rb index 66fe413e94884c..b77caea8f616df 100644 --- a/ee/spec/requests/api/go_proxy_spec.rb +++ b/ee/spec/requests/api/go_proxy_spec.rb @@ -3,440 +3,342 @@ require 'spec_helper' describe API::GoProxy do - let_it_be(:domain) do - port = ::Gitlab.config.gitlab.port - host = ::Gitlab.config.gitlab.host - case port when 80, 443 then host else "#{host}:#{port}" end - end + include EE::PackagesManagerApiSpecHelpers let_it_be(:user) { create :user } let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } - let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } + let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user } let_it_be(:job) { create :ci_build, user: user } let_it_be(:pa_token) { create :personal_access_token, user: user } - # rubocop: disable Layout/IndentationConsistency let_it_be(:modules) do - create_version(1, 0, 0, create_file('README.md', 'Hi', commit_message: 'Add README.md')) - create_version(1, 0, 1, create_module) - create_version(1, 0, 2, create_package('pkg')) - create_version(1, 0, 3, create_module('mod')) - sha1 = create_file('y.go', "package a\n") - sha2 = create_module('v2') - create_version(2, 0, 0, create_file('v2/x.go', "package a\n")) - - { sha: [sha1, sha2] } + commits = [ + create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } ), + create(:go_module_commit, :module, project: project, tag: 'v1.0.1' ), + create(:go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' ), + create(:go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' ), + create(:go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } ), + create(:go_module_commit, :module, project: project, name: 'v2' ), + create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" }) + ] + + { sha: [commits[4].sha, commits[5].sha] } end - describe 'GET /projects/:id/packages/go/*module_name/@v/list' do - let(:resource) { "list" } + before do + project.add_developer(user) + stub_licensed_features(packages: true) - context 'for the root module' do - let(:module_name) { base } + modules + end - it 'returns v1.0.1, v1.0.2, v1.0.3' do - get_resource(user) + shared_examples 'an unavailable resource' do + it 'returns not found' do + get_resource(user) - expect_module_version_list('v1.0.1', 'v1.0.2', 'v1.0.3') - end + expect(response).to have_gitlab_http_status(:not_found) end + end - context 'for the package' do - let(:module_name) { "#{base}/pkg" } + shared_examples 'a module version list resource' do |*versions, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "list" } - it 'returns nothing' do - get_resource(user) + it "returns #{versions.empty? ? 'nothing' : versions.join(', ')}" do + get_resource(user) - expect_module_version_list - end + expect(response).to have_gitlab_http_status(:ok) + expect(response.body.split("\n").to_set).to eq(versions.to_set) end + end - context 'for the submodule' do - let(:module_name) { "#{base}/mod" } + shared_examples 'a missing module version list resource' do |*versions, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "list" } - it 'returns v1.0.3' do - get_resource(user) + it_behaves_like 'an unavailable resource' + end - expect_module_version_list('v1.0.3') - end - end + shared_examples 'a module version information resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.info" } - context 'for the root module v2' do - let(:module_name) { "#{base}/v2" } + it "returns information for #{version}" do + get_resource(user) - it 'returns v2.0.0' do - get_resource(user) + time = project.repository.find_tag(version).dereferenced_target.committed_date - expect_module_version_list('v2.0.0') - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + expect(json_response['Time']).to eq(time.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')) end end - describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + shared_examples 'a missing module version information resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } let(:resource) { "#{version}.info" } - context 'with the root module v1.0.1' do - let(:module_name) { base } - let(:version) { "v1.0.1" } + it_behaves_like 'an unavailable resource' + end - it 'returns correct information' do - get_resource(user) + shared_examples 'a module pseudo-version information resource' do |prefix, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:commit) { project.repository.commit_by(oid: sha) } + let(:version) { fmt_pseudo_version prefix, commit } + let(:resource) { "#{version}.info" } - expect_module_version_info(version) - end + it "returns information for #{prefix}yyyymmddhhmmss-abcdefabcdef" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + expect(json_response['Time']).to eq(commit.committed_date.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')) end + end - context 'with the submodule v1.0.3' do - let(:module_name) { "#{base}/mod" } - let(:version) { "v1.0.3" } + shared_examples 'a missing module pseudo-version information resource' do |path: ''| + let(:module_name) { "#{base}#{path}" } + let(:commit) do + raise "tried to reference :commit without defining :sha" unless defined?(sha) - it 'returns correct information' do - get_resource(user) + project.repository.commit_by(oid: sha) + end + let(:resource) { "#{version}.info" } - expect_module_version_info(version) - end + it_behaves_like 'an unavailable resource' + end + + shared_examples 'a module file resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.mod" } + + it "returns #{path}/go.mod from the repo" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body.split("\n", 2).first).to eq("module #{module_name}") end + end - context 'with the root module v2.0.0' do - let(:module_name) { "#{base}/v2" } - let(:version) { "v2.0.0" } + shared_examples 'a missing module file resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.mod" } - it 'returns correct information' do - get_resource(user) + it_behaves_like 'an unavailable resource' + end + + shared_examples 'a module archive resource' do |version, entries, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.zip" } - expect_module_version_info(version) + it "returns an archive of #{path.empty? ? '/' : path} @ #{version} from the repo" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + + entries = entries.map { |e| "#{module_name}@#{version}/#{e}" }.to_set + actual = Set[] + Zip::InputStream.open(StringIO.new(response.body)) do |zip| + while (entry = zip.get_next_entry) + actual.add(entry.name) + end end + + expect(actual).to eq(entries) end + end - context 'with an invalid path' do - let(:module_name) { "#{base}/pkg" } - let(:version) { "v1.0.3" } + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + context 'for the root module' do + it_behaves_like 'a module version list resource', 'v1.0.1', 'v1.0.2', 'v1.0.3' + end - it 'returns not found' do - get_resource(user) + context 'for the package' do + it_behaves_like 'a module version list resource', path: '/pkg' + end - expect(response).to have_gitlab_http_status(:not_found) - end + context 'for the submodule' do + it_behaves_like 'a module version list resource', 'v1.0.3', path: '/mod' end - context 'with an invalid version' do - let(:module_name) { "#{base}/mod" } - let(:version) { "v1.0.1" } + context 'for the root module v2' do + it_behaves_like 'a module version list resource', 'v2.0.0', path: '/v2' + end + end - it 'returns not found' do - get_resource(user) + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with the root module v1.0.1' do + it_behaves_like 'a module version information resource', 'v1.0.1' + end - expect(response).to have_gitlab_http_status(:not_found) - end + context 'with the submodule v1.0.3' do + it_behaves_like 'a module version information resource', 'v1.0.3', path: '/mod' end - context 'with a pseudo-version for v1' do - let(:module_name) { base } - let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } - let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" } + context 'with the root module v2.0.0' do + it_behaves_like 'a module version information resource', 'v2.0.0', path: '/v2' + end - it 'returns the correct commit' do - get_resource(user) + context 'with an invalid path' do + it_behaves_like 'a missing module version information resource', 'v1.0.3', path: '/pkg' + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_kind_of(Hash) - expect(json_response['Version']).to eq(version) - expect(json_response['Time']).to eq(commit.committed_date.strftime '%Y-%m-%dT%H:%M:%S.%L%:z') + context 'with an invalid version' do + it_behaves_like 'a missing module version information resource', 'v1.0.1', path: '/mod' + end + + context 'with a pseudo-version for v1' do + it_behaves_like 'a module pseudo-version information resource', 'v1.0.4-0.' do + let(:sha) { modules[:sha][0] } end end context 'with a pseudo-version for v2' do - let(:module_name) { "#{base}/v2" } - let(:commit) { project.repository.commit_by(oid: modules[:sha][1]) } - let(:version) { "v2.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" } - - it 'returns the correct commit' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_kind_of(Hash) - expect(json_response['Version']).to eq(version) - expect(json_response['Time']).to eq(commit.committed_date.strftime '%Y-%m-%dT%H:%M:%S.%L%:z') + it_behaves_like 'a module pseudo-version information resource', 'v2.0.0-', path: '/v2' do + let(:sha) { modules[:sha][1] } end end context 'with a pseudo-version with an invalid timestamp' do - let(:module_name) { base } - let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } - let(:version) { "v1.0.4-0.00000000000000-#{commit.sha[0..11]}" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) + it_behaves_like 'a missing module pseudo-version information resource' do + let(:version) { "v1.0.4-0.00000000000000-#{modules[:sha][0][0..11]}" } end end context 'with a pseudo-version with an invalid commit sha' do - let(:module_name) { base } - let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } - let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-000000000000" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) + it_behaves_like 'a missing module pseudo-version information resource' do + let(:sha) { modules[:sha][0] } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-000000000000" } end end context 'with a pseudo-version with a short commit sha' do - let(:module_name) { base } - let(:commit) { project.repository.commit_by(oid: modules[:sha][0]) } - let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..10]}" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) + it_behaves_like 'a missing module pseudo-version information resource' do + let(:sha) { modules[:sha][0] } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{modules[:sha][0][0..10]}" } end end end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.mod' do - let(:resource) { "#{version}.mod" } - context 'with the root module v1.0.1' do - let(:module_name) { base } - let(:version) { "v1.0.1" } - - it 'returns correct content' do - get_resource(user) - - expect_module_version_mod(module_name) - end + it_behaves_like 'a module file resource', 'v1.0.1' end context 'with the submodule v1.0.3' do - let(:module_name) { "#{base}/mod" } - let(:version) { "v1.0.3" } - - it 'returns correct content' do - get_resource(user) - - expect_module_version_mod(module_name) - end + it_behaves_like 'a module file resource', 'v1.0.3', path: '/mod' end context 'with the root module v2.0.0' do - let(:module_name) { "#{base}/v2" } - let(:version) { "v2.0.0" } - - it 'returns correct content' do - get_resource(user) - - expect_module_version_mod(module_name) - end + it_behaves_like 'a module file resource', 'v2.0.0', path: '/v2' end context 'with an invalid path' do - let(:module_name) { "#{base}/pkg" } - let(:version) { "v1.0.3" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end + it_behaves_like 'a missing module file resource', 'v1.0.3', path: '/pkg' end context 'with an invalid version' do - let(:module_name) { "#{base}/mod" } - let(:version) { "v1.0.1" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end + it_behaves_like 'a missing module file resource', 'v1.0.1', path: '/mod' end end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.zip' do - let(:resource) { "#{version}.zip" } - context 'with the root module v1.0.1' do - let(:module_name) { base } - let(:version) { "v1.0.1" } - - it 'returns a zip of everything' do - get_resource(user) - - expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go']) - end + it_behaves_like 'a module archive resource', 'v1.0.1', ['README.md', 'go.mod', 'a.go'] end context 'with the root module v1.0.2' do - let(:module_name) { base } - let(:version) { "v1.0.2" } - - it 'returns a zip of everything' do - get_resource(user) - - expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) - end + it_behaves_like 'a module archive resource', 'v1.0.2', ['README.md', 'go.mod', 'a.go', 'pkg/b.go'] end context 'with the root module v1.0.3' do - let(:module_name) { base } - let(:version) { "v1.0.3" } - - it 'returns a zip of everything, excluding the submodule' do - get_resource(user) - - expect_module_version_zip(module_name, version, ['README.md', 'go.mod', 'a.go', 'pkg/b.go']) - end + it_behaves_like 'a module archive resource', 'v1.0.3', ['README.md', 'go.mod', 'a.go', 'pkg/b.go'] end context 'with the submodule v1.0.3' do - let(:module_name) { "#{base}/mod" } - let(:version) { "v1.0.3" } - - it 'returns a zip of the submodule' do - get_resource(user) - - expect_module_version_zip(module_name, version, ['go.mod', 'a.go']) - end + it_behaves_like 'a module archive resource', 'v1.0.3', ['go.mod', 'a.go'], path: '/mod' end context 'with the root module v2.0.0' do - let(:module_name) { "#{base}/v2" } - let(:version) { "v2.0.0" } - - it 'returns a zip of v2 of the root module' do - get_resource(user) - - expect_module_version_zip(module_name, version, ['go.mod', 'a.go', 'x.go']) - end + it_behaves_like 'a module archive resource', 'v2.0.0', ['go.mod', 'a.go', 'x.go'], path: '/v2' end end context 'with an invalid module directive' do let_it_be(:project) { create :project_empty_repo, :public, creator: user } - let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } + let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } - # rubocop: disable Layout/IndentationWidth let_it_be(:modules) do - create_file('a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") - create_version(1, 0, 0, create_file('go.mod', "module not/a/real/module\n")) - create_file('v2/a.go', "package a\nfunc Hi() { println(\"Hello world!\") }\n") - create_version(2, 0, 0, create_file('v2/go.mod', "module #{base}\n")) - - project.repository.head_commit + create(:go_module_commit, :files, project: project, files: { 'a.go' => "package\a" } ) + create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'go.mod' => "module not/a/real/module\n" }) + create(:go_module_commit, :files, project: project, files: { 'v2/a.go' => "package a\n" } ) + create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/go.mod' => "module #{base}\n" } ) end describe 'GET /projects/:id/packages/go/*module_name/@v/list' do - let(:resource) { "list" } - context 'with a completely wrong directive for v1' do - let(:module_name) { base } - - it 'returns nothing' do - get_resource(user) - - expect_module_version_list - end + it_behaves_like 'a module version list resource' end context 'with a directive omitting the suffix for v2' do - let(:module_name) { "#{base}/v2" } - - it 'returns nothing' do - get_resource(user) - - expect_module_version_list - end + it_behaves_like 'a module version list resource', path: '/v2' end end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do context 'with a completely wrong directive for v1' do - let(:module_name) { base } - let(:resource) { "v1.0.0.info" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end + it_behaves_like 'a missing module version information resource', 'v1.0.0' end context 'with a directive omitting the suffix for v2' do - let(:module_name) { "#{base}/v2" } - let(:resource) { "v2.0.0.info" } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) - end + it_behaves_like 'a missing module version information resource', 'v2.0.0', path: '/v2' end end end context 'with a case sensitive project and versions' do let_it_be(:project) { create :project_empty_repo, :public, creator: user, path: 'MyGoLib' } - let_it_be(:base) { "#{domain}/#{project.path_with_namespace}" } + let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } let_it_be(:base_encoded) { base.gsub(/[A-Z]/) { |s| "!#{s.downcase}"} } let_it_be(:modules) do - create_file('README.md', 'Hi', commit_message: 'Add README.md') - create_version(1, 0, 1, create_module, prerelease: 'prerelease') - create_version(1, 0, 1, create_package('pkg'), prerelease: 'Prerelease') - - project.repository.head_commit + create(:go_module_commit, :files, project: project, files: { 'README.md' => "Hi" }) + create(:go_module_commit, :module, project: project, tag: 'v1.0.1-prerelease') + create(:go_module_commit, :package, project: project, tag: 'v1.0.1-Prerelease', path: 'pkg') end describe 'GET /projects/:id/packages/go/*module_name/@v/list' do let(:resource) { "list" } context 'with a case encoded path' do - let(:module_name) { base_encoded } - - it 'returns the tags' do - get_resource(user) - - expect_module_version_list('v1.0.1-prerelease', 'v1.0.1-Prerelease') + it_behaves_like 'a module version list resource', 'v1.0.1-prerelease', 'v1.0.1-Prerelease' do + let(:module_name) { base_encoded } end end context 'without a case encoded path' do - let(:module_name) { base.downcase } - - it 'returns not found' do - get_resource(user) - - expect(response).to have_gitlab_http_status(:not_found) + it_behaves_like 'a missing module version list resource' do + let(:module_name) { base.downcase } end end end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do context 'with a case encoded path' do - let(:module_name) { base_encoded } - let(:resource) { "v1.0.1-!prerelease.info" } - - it 'returns the uppercase tag' do - get_resource(user) - - expect_module_version_info('v1.0.1-Prerelease') + it_behaves_like 'a module version information resource', 'v1.0.1-Prerelease' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-!prerelease.info" } end end context 'without a case encoded path' do - let(:module_name) { base_encoded } - let(:resource) { "v1.0.1-prerelease.info" } - - it 'returns the lowercase tag' do - get_resource(user) - - expect_module_version_info('v1.0.1-prerelease') + it_behaves_like 'a module version information resource', 'v1.0.1-prerelease' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-prerelease.info" } end end end @@ -467,6 +369,11 @@ expect(response).to have_gitlab_http_status(:ok) end + it 'returns ok with a personal access token and basic authentication' do + get_resource(headers: build_basic_auth_header(user.username, pa_token.token)) + expect(response).to have_gitlab_http_status(:ok) + end + it 'returns unauthorized with no authentication' do get_resource expect(response).to have_gitlab_http_status(:unauthorized) @@ -524,106 +431,11 @@ def get_resource(user = nil, **params) end end - before do - project.add_developer(user) - stub_licensed_features(packages: true) - - modules - end - - def get_resource(user = nil, **params) - get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params) - end - - def create_file(path, content, commit_message: 'Add file') - get_result("create file", Files::CreateService.new( - project, - project.owner, - commit_message: commit_message, - start_branch: 'master', - branch_name: 'master', - file_path: path, - file_content: content - ).execute) - end - - def create_package(path, commit_message: 'Add package') - get_result("create package '#{path}'", Files::MultiService.new( - project, - project.owner, - commit_message: commit_message, - start_branch: project.repository.root_ref, - branch_name: project.repository.root_ref, - actions: [ - { action: :create, file_path: path + '/b.go', content: "package b\nfunc Bye() { println(\"Goodbye world!\") }\n" } - ] - ).execute) - end - - def create_module(path = '', commit_message: 'Add module') - name = "#{domain}/#{project.path_with_namespace}" - if path != '' - name += '/' + path - path += '/' - end - - get_result("create module '#{name}'", Files::MultiService.new( - project, - project.owner, - commit_message: commit_message, - start_branch: project.repository.root_ref, - branch_name: project.repository.root_ref, - actions: [ - { action: :create, file_path: path + 'go.mod', content: "module #{name}\n" }, - { action: :create, file_path: path + 'a.go', content: "package a\nfunc Hi() { println(\"Hello world!\") }\n" } - ] - ).execute) - end - - def create_version(major, minor, patch, sha, prerelease: nil, build: nil, tag_message: nil) - name = "v#{major}.#{minor}.#{patch}" - name += "-#{prerelease}" if prerelease - name += "+#{build}" if build - - get_result("create version #{name[1..]}", Tags::CreateService.new(project, project.owner).execute(name, sha, tag_message)) - end - - def get_result(op, ret) - raise "#{op} failed: #{ret}" unless ret[:status] == :success - - ret[:result] - end - - def expect_module_version_list(*versions) - expect(response).to have_gitlab_http_status(:ok) - expect(response.body.split("\n").to_set).to eq(versions.to_set) - end - - def expect_module_version_info(version) - time = project.repository.find_tag(version).dereferenced_target.committed_date - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_kind_of(Hash) - expect(json_response['Version']).to eq(version) - expect(json_response['Time']).to eq(time.strftime '%Y-%m-%dT%H:%M:%S.%L%:z') + def get_resource(user = nil, headers: {}, **params) + get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params), headers: headers end - def expect_module_version_mod(name) - expect(response).to have_gitlab_http_status(:ok) - expect(response.body.split("\n", 2).first).to eq("module #{name}") - end - - def expect_module_version_zip(path, version, entries) - expect(response).to have_gitlab_http_status(:ok) - - entries = entries.map { |e| "#{path}@#{version}/#{e}" }.to_set - actual = Set[] - Zip::InputStream.open(StringIO.new(response.body)) do |zip| - while (entry = zip.get_next_entry) - actual.add(entry.name) - end - end - - expect(actual).to eq(entries) + def fmt_pseudo_version(prefix, commit) + "#{prefix}#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" end end -- GitLab From 709c5061772d91c69938371f329e679f03c3e575 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Fri, 17 Apr 2020 17:13:13 -0500 Subject: [PATCH 11/19] Improve Go module version processing - Clean up pseudo-version logic - Batch and memoize fetching blobs for a version + Fixes Gitaly N+1 - Remove archive generation from GoProxy (into GoModuleVersion) - Test module finder for path traversal - Also, add GitLab version to GoProxy API details --- ee/app/finders/packages/go/module_finder.rb | 1 + ee/app/finders/packages/go/version_finder.rb | 2 +- ee/app/models/packages/go_module_version.rb | 29 ++++++++++- ee/lib/api/go_proxy.rb | 20 +++----- .../api/helpers/packages/go/module_helpers.rb | 31 +++++++++--- ee/spec/factories/go_module_versions.rb | 2 +- ee/spec/factories/go_modules.rb | 2 +- .../finders/packages/go/module_finder_spec.rb | 42 ++++++++++++++-- .../packages/go/version_finder_spec.rb | 6 +-- .../models/packages/go_module_version_spec.rb | 50 +++++++++++++++++-- 10 files changed, 150 insertions(+), 35 deletions(-) diff --git a/ee/app/finders/packages/go/module_finder.rb b/ee/app/finders/packages/go/module_finder.rb index 7c4e382c24aa18..7b18a414552a51 100644 --- a/ee/app/finders/packages/go/module_finder.rb +++ b/ee/app/finders/packages/go/module_finder.rb @@ -10,6 +10,7 @@ class ModuleFinder attr_reader :project, :module_name def initialize(project, module_name) + module_name = CGI.unescape(module_name) module_name = Pathname.new(module_name).cleanpath.to_s @project = project diff --git a/ee/app/finders/packages/go/version_finder.rb b/ee/app/finders/packages/go/version_finder.rb index 7504554e253c7c..c8c4778355af10 100644 --- a/ee/app/finders/packages/go/version_finder.rb +++ b/ee/app/finders/packages/go/version_finder.rb @@ -51,7 +51,7 @@ def find_commit(commit) def find_pseudo_version(str) semver = parse_semver(str) - raise ArgumentError.new 'target is not a pseudo-version' unless semver && PSEUDO_VERSION_REGEX.match?(str) + raise ArgumentError.new 'target is not a pseudo-version' unless pseudo_version?(semver) # valid pseudo-versions are # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index 3575d1361ceb73..3bfc40ff3e6ad4 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -37,10 +37,25 @@ def name @name || @ref&.name end + def full_name + "#{mod.name}@#{name || commit.sha}" + end + def gomod @gomod ||= blob_at(@mod.path + '/go.mod') end + def archive + suffix_len = @mod.path == '' ? 0 : @mod.path.length + 1 + + Zip::OutputStream.write_buffer do |zip| + files.each do |file| + zip.put_next_entry "#{full_name}/#{file.path[suffix_len...]}" + zip.write blob_at(file.path) + end + end + end + def files return @files if defined?(@files) @@ -51,10 +66,22 @@ def files end def blob_at(path) - @mod.project.repository.blob_at(@commit.sha, path)&.data + return if path.nil? || path.empty? + + path = path[1..] if path.start_with? '/' + + blobs.find { |x| x.path == path }&.data end def valid? @mod.path_valid?(major) && @mod.gomod_valid?(gomod) end + + private + + def blobs + return @blobs if defined?(@blobs) + + @blobs = @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x.path] }) + end end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index fe045204c1da25..6e8c95071d9797 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -66,7 +66,7 @@ def find_version namespace ':id/packages/go/*module_name/@v' do desc 'Get all tagged versions for a given Go module' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/list' + detail 'See `go help goproxy`, GET $GOPROXY//@v/list. This feature was introduced in GitLab 13.0.' end get 'list' do mod = find_module @@ -76,7 +76,7 @@ def find_version end desc 'Get information about the given module version' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/.info' + detail 'See `go help goproxy`, GET $GOPROXY//@v/.info. This feature was introduced in GitLab 13.0.' success EE::API::Entities::GoModuleVersion end params do @@ -89,7 +89,7 @@ def find_version end desc 'Get the module file of the given module version' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod' + detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod. This feature was introduced in GitLab 13.0.' end params do requires :module_version, type: String, desc: 'Module version' @@ -102,7 +102,7 @@ def find_version end desc 'Get a zip of the source of the given module version' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip' + detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip. This feature was introduced in GitLab 13.0.' end params do requires :module_version, type: String, desc: 'Module version' @@ -110,21 +110,13 @@ def find_version get ':module_version.zip', requirements: MODULE_VERSION_REQUIREMENTS do ver = find_version - suffix_len = ver.mod.path == '' ? 0 : ver.mod.path.length + 1 - - s = Zip::OutputStream.write_buffer do |zip| - ver.files.each do |file| - zip.put_next_entry "#{ver.mod.name}@#{ver.name}/#{file.path[suffix_len...]}" - zip.write ver.blob_at(file.path) - end - end - + # TODO: Content-Type should be application/zip, see #214876 header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: ver.name + '.zip') header['Content-Transfer-Encoding'] = 'binary' content_type 'text/plain' # content_type 'application/zip' status :ok - body s.string + body ver.archive.string end end end diff --git a/ee/lib/api/helpers/packages/go/module_helpers.rb b/ee/lib/api/helpers/packages/go/module_helpers.rb index 375c44460cf136..b707feda763f95 100644 --- a/ee/lib/api/helpers/packages/go/module_helpers.rb +++ b/ee/lib/api/helpers/packages/go/module_helpers.rb @@ -5,12 +5,6 @@ module Helpers module Packages module Go module ModuleHelpers - # basic semver regex - SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze - - # semver, but the prerelease component follows a specific format - PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze - def case_encode(str) str.gsub(/A-Z/) { |s| "!#{s.downcase}"} end @@ -25,8 +19,29 @@ def semver?(tag) ::Packages::SemVer.match?(tag.name, prefixed: true) end - def pseudo_version?(str) - ::Packages::SemVer.match?(str, prefixed: true) && PSEUDO_VERSION_REGEX.match?(str) + def pseudo_version?(version) + return false unless version + + if version.is_a? String + version = parse_semver version + return false unless version + end + + pre = version.prerelease + + # valid pseudo-versions are + # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X + # vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre + # vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z + + if version.minor != 0 || version.patch != 0 + m = /\A(.*\.)?0\./.freeze.match pre + return false unless m + + pre = pre[m[0].length..] + end + + /\A\d{14}-[A-Za-z0-9]+\z/.freeze.match? pre end def parse_semver(str) diff --git a/ee/spec/factories/go_module_versions.rb b/ee/spec/factories/go_module_versions.rb index d87c9e31f82e3b..7e562c863678e1 100644 --- a/ee/spec/factories/go_module_versions.rb +++ b/ee/spec/factories/go_module_versions.rb @@ -19,7 +19,7 @@ def get_result(op, ret) new(p.mod, p.type, p.commit, name: p.name, semver: s, ref: p.ref) end - mod { go_module } + mod { create :go_module } type { :commit } commit { raise ArgumentError.new("commit is required") } name { nil } diff --git a/ee/spec/factories/go_modules.rb b/ee/spec/factories/go_modules.rb index 5f5fbacc0e0c5c..bec33bdd7b8aa7 100644 --- a/ee/spec/factories/go_modules.rb +++ b/ee/spec/factories/go_modules.rb @@ -7,6 +7,6 @@ project path { '' } - name { "#{Settings.build_gitlab_go_url}/#{project.full_path}#{path.empty? ? '' : path}" } + name { "#{Settings.build_gitlab_go_url}/#{project.full_path}#{path.empty? ? '' : '/'}#{path}" } end end diff --git a/ee/spec/finders/packages/go/module_finder_spec.rb b/ee/spec/finders/packages/go/module_finder_spec.rb index 914812ebed41ff..307f823047bcb0 100644 --- a/ee/spec/finders/packages/go/module_finder_spec.rb +++ b/ee/spec/finders/packages/go/module_finder_spec.rb @@ -5,10 +5,25 @@ describe Packages::Go::ModuleFinder do let_it_be(:project) { create :project } let_it_be(:other_project) { create :project } + let(:finder) { described_class.new project, module_name } + + shared_examples 'an invalid path' do + describe '#module_name' do + it 'returns the expected name' do + expect(finder.module_name).to eq(expected_name) + end + end + + describe '#execute' do + it 'returns nil' do + expect(finder.execute).to be_nil + end + end + end describe '#execute' do context 'with module name equal to project name' do - let(:finder) { described_class.new(project, base_url(project)) } + let(:module_name) { base_url(project) } it 'returns a module with empty path' do mod = finder.execute @@ -18,7 +33,7 @@ end context 'with module name starting with project name and slash' do - let(:finder) { described_class.new(project, base_url(project) + '/mod') } + let(:module_name) { base_url(project) + '/mod' } it 'returns a module with non-empty path' do mod = finder.execute @@ -28,7 +43,7 @@ end context 'with a module name not equal to and not starting with project name' do - let(:finder) { described_class.new(project, base_url(other_project)) } + let(:module_name) { base_url(other_project) } it 'returns nil' do expect(finder.execute).to be_nil @@ -36,6 +51,27 @@ end end + context 'with relative path component' do + it_behaves_like 'an invalid path' do + let(:module_name) { base_url(project) + '/../xyz' } + let(:expected_name) { base_url(project.namespace) + '/xyz' } + end + end + + context 'with a URL encoded relative path component' do + it_behaves_like 'an invalid path' do + let(:module_name) { base_url(project) + '/%2E%2E%2Fxyz' } + let(:expected_name) { base_url(project.namespace) + '/xyz' } + end + end + + context 'with many relative path components' do + it_behaves_like 'an invalid path' do + let(:module_name) { base_url(project) + ('/..' * 10) + '/xyz' } + let(:expected_name) { ('../' * 7) + 'xyz' } + end + end + def base_url(project) "#{Settings.build_gitlab_go_url}/#{project.full_path}" end diff --git a/ee/spec/finders/packages/go/version_finder_spec.rb b/ee/spec/finders/packages/go/version_finder_spec.rb index 1459e693345b95..0789091ae7aa0c 100644 --- a/ee/spec/finders/packages/go/version_finder_spec.rb +++ b/ee/spec/finders/packages/go/version_finder_spec.rb @@ -39,19 +39,19 @@ end context 'for the package' do - let(:mod) { create :go_module, project: project, path: '/pkg' } + let(:mod) { create :go_module, project: project, path: 'pkg' } it_behaves_like '#execute' end context 'for the submodule' do - let(:mod) { create :go_module, project: project, path: '/mod' } + let(:mod) { create :go_module, project: project, path: 'mod' } it_behaves_like '#execute', 'v1.0.3' end context 'for the root module v2' do - let(:mod) { create :go_module, project: project, path: '/v2' } + let(:mod) { create :go_module, project: project, path: 'v2' } it_behaves_like '#execute', 'v2.0.0' end diff --git a/ee/spec/models/packages/go_module_version_spec.rb b/ee/spec/models/packages/go_module_version_spec.rb index 2db15a702ab105..d76d924201f7e9 100644 --- a/ee/spec/models/packages/go_module_version_spec.rb +++ b/ee/spec/models/packages/go_module_version_spec.rb @@ -17,6 +17,28 @@ create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } end + shared_examples '#files' do |desc, *entries| + it "returns #{desc}" do + actual = version.files.map { |x| x.path }.to_set + expect(actual).to eq(entries.to_set) + end + end + + shared_examples '#archive' do |desc, *entries| + it "returns an archive of #{desc}" do + expected = entries.map { |e| "#{version.full_name}/#{e}" }.to_set + + actual = Set[] + Zip::InputStream.open(StringIO.new(version.archive.string)) do |zip| + while (entry = zip.get_next_entry) + actual.add(entry.name) + end + end + + expect(actual).to eq(expected) + end + end + describe '#name' do context 'with ref and name specified' do let_it_be(:version) { create :go_module_version, mod: mod, name: 'foobar', commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } @@ -50,20 +72,42 @@ context 'with a root module' do context 'with an empty module path' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } - it('returns all the files') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) } + it_behaves_like '#files', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' + end + end + + context 'with a root module and a submodule' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#files', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' + end + + context 'with the submodule\'s path' do + let_it_be(:mod) { create :go_module, project: project, path: 'mod' } + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#files', 'the submodule\'s files', 'mod/go.mod', 'mod/a.go' + end + end + end + + describe '#archive' do + context 'with a root module' do + context 'with an empty module path' do + let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } + it_behaves_like '#archive', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' end end context 'with a root module and a submodule' do context 'with an empty module path' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } - it('returns files excluding the submodule') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['README.md', 'go.mod', 'a.go', 'pkg/b.go']) } + it_behaves_like '#archive', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' end context 'with the submodule\'s path' do let_it_be(:mod) { create :go_module, project: project, path: 'mod' } let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } - it('returns the submodule\'s files') { expect(version.files.map { |x| x.path }.to_set).to eq(Set['mod/go.mod', 'mod/a.go']) } + it_behaves_like '#archive', 'the submodule\'s files', 'go.mod', 'a.go' end end end -- GitLab From 255321edfdc5b3c0bc01c7961ad75b6cb1a08109 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Fri, 17 Apr 2020 17:07:56 -0500 Subject: [PATCH 12/19] Update Go proxy documentation - Resolve discussions on !27746 - Document fetching private packages via `.netrc` - Include required scope for API token --- doc/administration/packages/index.md | 2 +- doc/user/packages/go_proxy/index.md | 47 +++++++++++++++++++++---- doc/user/packages/index.md | 2 +- ee/lib/api/go_proxy.rb | 1 - ee/spec/factories/go_module_commits.rb | 6 ---- ee/spec/factories/go_module_versions.rb | 6 ---- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index ac9b342d2d8af7..7fbe942cb2d293 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -13,7 +13,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [Conan Repository](../../user/packages/conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ | | [Maven Repository](../../user/packages/maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | | [NPM Registry](../../user/packages/npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | -| [Go Proxy](../../user/packages/go_proxy/index.md) | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | ??.??+ | +| [Go Proxy](../../user/packages/go_proxy/index.md) | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | Don't you see your package management system supported yet? Please consider contributing diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index 3d235f832c3284..092f95dce02540 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -1,6 +1,6 @@ # GitLab Go Proxy **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab Premium](https://about.gitlab.com/pricing/) ??.??. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0. The GitLab Go Proxy implements the Go proxy protocol. @@ -9,7 +9,7 @@ GitLab does not (yet) display Go modules in the **Packages** section of a project. Only the Go proxy protocol is supported at this time, and only for modules on GitLab. -## Enabling the Go proxy +## Enable the Go proxy NOTE: **Note:** This option is available only if your GitLab administrator has @@ -25,7 +25,7 @@ by default. To enable it for existing projects, or if you want to disable it: You should then be able to see the **Packages** section on the left sidebar. Next, you must configure your development environment to use the Go proxy. -## Adding GitLab as a Go proxy +## Add GitLab as a Go proxy NOTE: **Note:** To use a Go proxy, you must be using Go 1.13 or later. @@ -51,7 +51,40 @@ first go through GitLab. This can help avoid making requests for private packages to the public proxy, but `GOPRIVATE` is a much safer way of achieving that. -## Releasing a module +For example, with the following configuration, Go will attempt to fetch modules +from 1) GitLab project 1234's Go module proxy, 2) `proxy.golang.org`, and +finally 3) directly via VCS. + +```shell +go env -w GOPROXY=https://gitlab.com/api/v4/projects/1234/packages/go,https://proxy.golang.org,direct +``` + +## Fetch modules from private projects + +NOTE: **Note:** +`go` does not support transmitting credentials over insecure connections. The +steps below will only work if GitLab is configured for HTTPS. + +GitLab's Go proxy implementation supports HTTP Basic authentication for personal +access tokens, in addition to the usual authentication mechanisms. To configure +Go to use HTTP Basic authentication, you must create a [personal access +token](../../profile/personal_access_tokens.md) with the `api` or `read_api` +scope and add it to [`~/.netrc`](https://ec.haxx.se/usingcurl/usingcurl-netrc): + +```netrc +machine my-server +login my-user +password my-token +``` + +Replace `my-user` with your username and `my-token` with your personal access +token. The value of `my-server` should be `gitlab.com` or the URL of your GitLab +instance. You can optionally append a path to `my-server`, which will restrict +the scope that the credentials will be used for. For example, `machine +gitlab.com/my-group` will restrict the credentials to URLs starting with +`gitlab.com/my-group`. + +## Release a module NOTE: **Note:** For a complete understanding of Go modules and versioning, see [this series of @@ -84,6 +117,6 @@ Tags that match the pseudo-version pattern are ignored, as otherwise they could interfere with fetching specific commits using a pseudo-version. Pseudo-versions follow one of three formats: -- `vX.0.0-yyyymmddhhmmss-abcdefabcdef`, when no earlier tagged commit exists for X -- `vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z-pre -- `vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z +- `vX.0.0-yyyymmddhhmmss-abcdefabcdef`, when no earlier tagged commit exists for X. +- `vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z-pre. +- `vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z. diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index e0f3a19ae94807..80d984e42b1042 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -21,7 +21,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [NPM Registry](npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | | [NuGet Repository](nuget_repository/index.md) **(PREMIUM)** | The GitLab NuGet Repository will enable every project in GitLab to have its own space to store [NuGet](https://www.nuget.org/) packages. | 12.8+ | | [PyPi Repository](pypi_repository/index.md) **(PREMIUM)** | The GitLab PyPi Repository will enable every project in GitLab to have its own space to store [PyPi](https://pypi.org/) packages. | 12.10+ | -| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | ??.??+ | +| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | ## Enable the Package Registry for your project diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index 6e8c95071d9797..fd10c1d60ad1ff 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -114,7 +114,6 @@ def find_version header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: ver.name + '.zip') header['Content-Transfer-Encoding'] = 'binary' content_type 'text/plain' - # content_type 'application/zip' status :ok body ver.archive.string end diff --git a/ee/spec/factories/go_module_commits.rb b/ee/spec/factories/go_module_commits.rb index 4dc3df88252bb9..aebca73efc04e4 100644 --- a/ee/spec/factories/go_module_commits.rb +++ b/ee/spec/factories/go_module_commits.rb @@ -1,11 +1,5 @@ # frozen_string_literal: true -def get_result(op, ret) - raise "#{op} failed: #{ret}" unless ret[:status] == :success - - ret[:result] -end - FactoryBot.define do factory :go_module_commit, class: 'Commit' do skip_create diff --git a/ee/spec/factories/go_module_versions.rb b/ee/spec/factories/go_module_versions.rb index 7e562c863678e1..993559190aea41 100644 --- a/ee/spec/factories/go_module_versions.rb +++ b/ee/spec/factories/go_module_versions.rb @@ -1,11 +1,5 @@ # frozen_string_literal: true -def get_result(op, ret) - raise "#{op} failed: #{ret}" unless ret[:status] == :success - - ret[:result] -end - FactoryBot.define do factory :go_module_version, class: 'Packages::GoModuleVersion' do skip_create -- GitLab From 6a68ec20ce9b46f594c626dc3379c4a53215a54f Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Mon, 4 May 2020 14:41:25 -0500 Subject: [PATCH 13/19] Go proxy documentation edits --- doc/administration/packages/index.md | 2 +- doc/user/packages/go_proxy/index.md | 47 +++++++++++++++------------- doc/user/packages/index.md | 2 +- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index 7fbe942cb2d293..dd8d5e9c6ae455 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -13,7 +13,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [Conan Repository](../../user/packages/conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ | | [Maven Repository](../../user/packages/maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | | [NPM Registry](../../user/packages/npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | -| [Go Proxy](../../user/packages/go_proxy/index.md) | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | +| [Go Proxy](../../user/packages/go_proxy/index.md) | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | Don't you see your package management system supported yet? Please consider contributing diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index 092f95dce02540..3b7633b6259c71 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -1,13 +1,10 @@ # GitLab Go Proxy **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab +> Premium](https://about.gitlab.com/pricing/) 13.0. -The GitLab Go Proxy implements the Go proxy protocol. - -NOTE: **Note:** -GitLab does not (yet) display Go modules in the **Packages** section of a -project. Only the Go proxy protocol is supported at this time, and only for -modules on GitLab. +With the Go proxy for GitLab, every project in GitLab can be fetched with the +[Go proxy protocol](https://proxy.golang.org/). ## Enable the Go proxy @@ -25,6 +22,11 @@ by default. To enable it for existing projects, or if you want to disable it: You should then be able to see the **Packages** section on the left sidebar. Next, you must configure your development environment to use the Go proxy. +NOTE: **Note:** +GitLab does not display Go modules in the **Packages** section of a project. +Only the Go proxy protocol is supported at this time, and only for modules on +GitLab. + ## Add GitLab as a Go proxy NOTE: **Note:** @@ -35,10 +37,11 @@ The available proxy endpoints are: - Project - can fetch modules defined by a project - `/api/v4/projects/:id/packages/go` Go's use of proxies is configured with the `GOPROXY` environment variable, as a -comma separated list of URLs. Go 1.14 adds support for managing Go's environment -variables via `go env -w`, e.g. `go env -w GOPROXY=...`. This will write to -`$GOPATH/env` (which defaults to `~/.go/env`). `GOPROXY` can also be configured -as a normal environment variable, via RC files or `export GOPROXY=...`. +comma separated list of URLs. Go 1.14 adds support for comma separated list of +URLs. Go 1.14 adds support for using `go env -w` to manage Go's environment +variables. For example, `go env -w GOPROXY=...` writes to to `$GOPATH/env` +(which defaults to `~/.go/env`). `GOPROXY` can also be configured as a normal +environment variable, with RC files or `export GOPROXY=...`. The default value of `$GOPROXY` is `https://proxy.golang.org,direct`, which tells `go` to first query `proxy.golang.org` and fallback to direct VCS @@ -53,7 +56,8 @@ that. For example, with the following configuration, Go will attempt to fetch modules from 1) GitLab project 1234's Go module proxy, 2) `proxy.golang.org`, and -finally 3) directly via VCS. +finally 3) directly with Git (or another VCS, depending on where the module +source is hosted). ```shell go env -w GOPROXY=https://gitlab.com/api/v4/projects/1234/packages/go,https://proxy.golang.org,direct @@ -86,13 +90,8 @@ gitlab.com/my-group` will restrict the credentials to URLs starting with ## Release a module -NOTE: **Note:** -For a complete understanding of Go modules and versioning, see [this series of -blog posts](https://blog.golang.org/using-go-modules) on the official Go -website. - -Go modules and module versions are handled entirely via Git (or SVN, Mercurial, -etc). A module is a repository containing Go source and a `go.mod` file. A +Go modules and module versions are handled entirely with Git (or SVN, Mercurial, +and so on). A module is a repository containing Go source and a `go.mod` file. A version of a module is a Git tag (or equivalent) that is a valid [semantic version](https://semver.org), prefixed with 'v'. For example, `v1.0.0` and `v1.3.2-alpha` are valid module versions, but `v1` or `v1.2` are not. @@ -101,6 +100,10 @@ Go requires that major versions after v1 involve a change in the import path of the module. For example, version 2 of the module `gitlab.com/my/project` must be imported and released as `gitlab.com/my/project/v2`. +For a complete understanding of Go modules and versioning, see [this series of +blog posts](https://blog.golang.org/using-go-modules) on the official Go +website. + ## Valid modules and versions The GitLab Go proxy will ignore modules and module versions that have an invalid @@ -108,9 +111,9 @@ The GitLab Go proxy will ignore modules and module versions that have an invalid `gitlab.com/my/project` can be accessed via that same URL, and that the first line of `go.mod` is `module gitlab.com/my/project`. If `go.mod` names a different module, compilation will fail. Additionally, Go requires, for major -versions after 1, that the name of the module have an appropriate suffix, e.g. -`gitlab.com/my/project/v2`. If the `module` directive does not also have this -suffix, compilation will fail. +versions after 1, that the name of the module have an appropriate suffix, for +example `gitlab.com/my/project/v2`. If the `module` directive does not also have +this suffix, compilation will fail. Go supports 'pseudo-versions' that encode the timestamp and SHA of a commit. Tags that match the pseudo-version pattern are ignored, as otherwise they could diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index 80d984e42b1042..6e939d7daca633 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -21,7 +21,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [NPM Registry](npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | | [NuGet Repository](nuget_repository/index.md) **(PREMIUM)** | The GitLab NuGet Repository will enable every project in GitLab to have its own space to store [NuGet](https://www.nuget.org/) packages. | 12.8+ | | [PyPi Repository](pypi_repository/index.md) **(PREMIUM)** | The GitLab PyPi Repository will enable every project in GitLab to have its own space to store [PyPi](https://pypi.org/) packages. | 12.10+ | -| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The GitLab Go Proxy enables every project in GitLab to be fetched via the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | +| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | ## Enable the Package Registry for your project -- GitLab From 00da5590a9eb46e599093cd68bec6a8e55e53e60 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Mon, 4 May 2020 15:38:31 -0500 Subject: [PATCH 14/19] Refactor Go proxy documentation --- doc/user/packages/go_proxy/index.md | 70 ++++++++++++++--------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index 3b7633b6259c71..3fcfd28913bd09 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -6,27 +6,48 @@ With the Go proxy for GitLab, every project in GitLab can be fetched with the [Go proxy protocol](https://proxy.golang.org/). -## Enable the Go proxy +## Prerequisites -NOTE: **Note:** -This option is available only if your GitLab administrator has -[enabled support for the Package Registry](../../../administration/packages/index.md). **(PREMIUM ONLY)** - -After the Package Registry is enabled, it will be available for all new projects -by default. To enable it for existing projects, or if you want to disable it: +### Enable the Package Registry -1. Navigate to your project's **Settings > General > Permissions**. -1. Find the Packages feature and enable or disable it. -1. Click on **Save changes** for the changes to take effect. +The Package Registry is enabled for new projects by default. If you cannot find +the **{package}** **Packages > List** entry under your project's sidebar, verify +the following: -You should then be able to see the **Packages** section on the left sidebar. -Next, you must configure your development environment to use the Go proxy. +1. Your GitLab administrator has [enabled support for the Package + Registry](../../../administration/packages/index.md). **(PREMIUM ONLY)** +1. The Package Registry is [enabled for your project](../index.md). NOTE: **Note:** GitLab does not display Go modules in the **Packages** section of a project. Only the Go proxy protocol is supported at this time, and only for modules on GitLab. +### Fetch modules from private projects + +NOTE: **Note:** +`go` does not support transmitting credentials over insecure connections. The +steps below will only work if GitLab is configured for HTTPS. + +GitLab's Go proxy implementation supports HTTP Basic authentication for personal +access tokens, in addition to the usual authentication mechanisms. To configure +Go to use HTTP Basic authentication, you must create a [personal access +token](../../profile/personal_access_tokens.md) with the `api` or `read_api` +scope and add it to [`~/.netrc`](https://ec.haxx.se/usingcurl/usingcurl-netrc): + +```netrc +machine my-server +login my-user +password my-token +``` + +Replace `my-user` with your username and `my-token` with your personal access +token. The value of `my-server` should be `gitlab.com` or the URL of your GitLab +instance. You can optionally append a path to `my-server`, which will restrict +the scope that the credentials will be used for. For example, `machine +gitlab.com/my-group` will restrict the credentials to URLs starting with +`gitlab.com/my-group`. + ## Add GitLab as a Go proxy NOTE: **Note:** @@ -63,31 +84,6 @@ source is hosted). go env -w GOPROXY=https://gitlab.com/api/v4/projects/1234/packages/go,https://proxy.golang.org,direct ``` -## Fetch modules from private projects - -NOTE: **Note:** -`go` does not support transmitting credentials over insecure connections. The -steps below will only work if GitLab is configured for HTTPS. - -GitLab's Go proxy implementation supports HTTP Basic authentication for personal -access tokens, in addition to the usual authentication mechanisms. To configure -Go to use HTTP Basic authentication, you must create a [personal access -token](../../profile/personal_access_tokens.md) with the `api` or `read_api` -scope and add it to [`~/.netrc`](https://ec.haxx.se/usingcurl/usingcurl-netrc): - -```netrc -machine my-server -login my-user -password my-token -``` - -Replace `my-user` with your username and `my-token` with your personal access -token. The value of `my-server` should be `gitlab.com` or the URL of your GitLab -instance. You can optionally append a path to `my-server`, which will restrict -the scope that the credentials will be used for. For example, `machine -gitlab.com/my-group` will restrict the credentials to URLs starting with -`gitlab.com/my-group`. - ## Release a module Go modules and module versions are handled entirely with Git (or SVN, Mercurial, -- GitLab From 80613d98ab1736eeca523902b20ff8b7af64ca11 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Wed, 13 May 2020 18:24:53 -0500 Subject: [PATCH 15/19] Detail how to configure Go to skip checksum downloads --- doc/user/packages/go_proxy/index.md | 44 +++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index 3fcfd28913bd09..4ff8894e745d76 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -29,24 +29,38 @@ NOTE: **Note:** `go` does not support transmitting credentials over insecure connections. The steps below will only work if GitLab is configured for HTTPS. -GitLab's Go proxy implementation supports HTTP Basic authentication for personal -access tokens, in addition to the usual authentication mechanisms. To configure -Go to use HTTP Basic authentication, you must create a [personal access -token](../../profile/personal_access_tokens.md) with the `api` or `read_api` -scope and add it to [`~/.netrc`](https://ec.haxx.se/usingcurl/usingcurl-netrc): + 1. Configure Go to include HTTP basic authentication credentials when fetching from the Go proxy for GitLab. + 2. Configure Go to *not* attempt to download checksums for private GitLab projects from the public checksum database. + +#### Enable Request Authentication + +Create a [personal access token](../../profile/personal_access_tokens.md) with +the `api` or `read_api` scope and add it to +[`~/.netrc`](https://ec.haxx.se/usingcurl/usingcurl-netrc): ```netrc -machine my-server -login my-user -password my-token +machine login password ``` -Replace `my-user` with your username and `my-token` with your personal access -token. The value of `my-server` should be `gitlab.com` or the URL of your GitLab -instance. You can optionally append a path to `my-server`, which will restrict -the scope that the credentials will be used for. For example, `machine -gitlab.com/my-group` will restrict the credentials to URLs starting with -`gitlab.com/my-group`. +`` should be the URL of the GitLab instance, for example `gitlab.com`. +`` and `` should be your username and the personal access +token, respectively. + +#### Disable Checksum Database + +By default, Go will query `sum.golang.org` for module checksums. This will not +work modules that are not public. `GONOSUMDB` can be used to disable downloading +checksums for specific URLs. This can be permanently set with `go env -w +GONOSUMDB=`. + +- `GONOSUMDB=gitlab.com/my/project` will disable checksum downloads for + `gitlab.com/my/project` +- `GONOSUMDB=gitlab.com/namespace` will disable checksum downloads for all + projects under `gitlab.com/namespace` +- `GONOSUMDB=gitlab.com` will disable checksum downloads for *all* modules on + GitLab.com +- `GOSUMDB=off` or `GONOSUMDB=*` will *completely* disable the checksum database + for all modules and packages. ## Add GitLab as a Go proxy @@ -104,7 +118,7 @@ website. The GitLab Go proxy will ignore modules and module versions that have an invalid `module` directive in their `go.mod`. Go requires that a package imported as -`gitlab.com/my/project` can be accessed via that same URL, and that the first +`gitlab.com/my/project` can be accessed with that same URL, and that the first line of `go.mod` is `module gitlab.com/my/project`. If `go.mod` names a different module, compilation will fail. Additionally, Go requires, for major versions after 1, that the name of the module have an appropriate suffix, for -- GitLab From 24d3e9b55ffb7f09e79916d330943f6ab65f9f5e Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Wed, 13 May 2020 19:54:25 -0500 Subject: [PATCH 16/19] Clean up and improve Go proxy documentation - Various fixes and tweaks - Improve checksum database documentation --- doc/user/packages/go_proxy/index.md | 81 ++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index 4ff8894e745d76..eaa0d38dbf6126 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -1,36 +1,69 @@ # GitLab Go Proxy **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab -> Premium](https://about.gitlab.com/pricing/) 13.0. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27376) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.1. +> - It's deployed behind a feature flag, disabled by default. +> - It's disabled on GitLab.com. +> - It's not recommended for production use. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-the-go-proxy). **(PREMIUM)** With the Go proxy for GitLab, every project in GitLab can be fetched with the [Go proxy protocol](https://proxy.golang.org/). ## Prerequisites +### Enable the Go proxy + +The Go proxy for GitLab is under development and not ready for production use, due to +[potential performance issues with large repositories](https://gitlab.com/gitlab-org/gitlab/-/issues/218083). + +It is deployed behind a feature flag that is **disabled by default**. + +[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can enable it for your instance. + +To enable it: + +```ruby +Feature.enable(:go_proxy) # or +``` + +To disable it: + +```ruby +Feature.disable(:go_proxy) +``` + +To enable or disable it for specific projects: + +```ruby +Feature.enable(:go_proxy, Project.find(1)) +Feature.disable(:go_proxy, Project.find(2)) +``` + ### Enable the Package Registry The Package Registry is enabled for new projects by default. If you cannot find the **{package}** **Packages > List** entry under your project's sidebar, verify the following: -1. Your GitLab administrator has [enabled support for the Package - Registry](../../../administration/packages/index.md). **(PREMIUM ONLY)** +1. Your GitLab administrator has + [enabled support for the Package Registry](../../../administration/packages/index.md). **(PREMIUM ONLY)** 1. The Package Registry is [enabled for your project](../index.md). NOTE: **Note:** -GitLab does not display Go modules in the **Packages** section of a project. -Only the Go proxy protocol is supported at this time, and only for modules on -GitLab. +GitLab does not currently display Go modules in the **Packages Registry** of a project. +Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213770) for details. ### Fetch modules from private projects NOTE: **Note:** `go` does not support transmitting credentials over insecure connections. The -steps below will only work if GitLab is configured for HTTPS. +steps below work only if GitLab is configured for HTTPS. - 1. Configure Go to include HTTP basic authentication credentials when fetching from the Go proxy for GitLab. - 2. Configure Go to *not* attempt to download checksums for private GitLab projects from the public checksum database. +1. Configure Go to include HTTP basic authentication credentials when fetching + from the Go proxy for GitLab. +1. Configure Go to skip downloading of checksums for private GitLab projects + from the public checksum database. #### Enable Request Authentication @@ -46,21 +79,21 @@ machine login password `` and `` should be your username and the personal access token, respectively. -#### Disable Checksum Database +#### Disable checksum database queries -By default, Go will query `sum.golang.org` for module checksums. This will not -work modules that are not public. `GONOSUMDB` can be used to disable downloading -checksums for specific URLs. This can be permanently set with `go env -w -GONOSUMDB=`. +Go can be configured to query a checksum database for module checksums. Go 1.13 +and later query `sum.golang.org` by default. This fails for modules that are not +public and thus not accessible to `sum.golang.org`. To resolve this issue, set +`GONOSUMDB` to a comma-separated list of projects or namespaces for which Go +should not query the checksum database. For example, `go env -w +GONOSUMDB=gitlab.com/my/project` persistently configures Go to skip checksum +queries for the project `gitlab.com/my/project`. -- `GONOSUMDB=gitlab.com/my/project` will disable checksum downloads for - `gitlab.com/my/project` -- `GONOSUMDB=gitlab.com/namespace` will disable checksum downloads for all - projects under `gitlab.com/namespace` -- `GONOSUMDB=gitlab.com` will disable checksum downloads for *all* modules on - GitLab.com -- `GOSUMDB=off` or `GONOSUMDB=*` will *completely* disable the checksum database - for all modules and packages. +Checksum database queries can be disabled for arbitrary prefixes or disabled +entirely. However, checksum database queries are a security mechanism and as +such they should be disabled selectively and only when necessary. `GOSUMDB=off` +or `GONOSUMDB=*` disables checksum queries entirely. `GONOSUMDB=gitlab.com` +disables checksum queries for all projects hosted on GitLab.com. ## Add GitLab as a Go proxy @@ -74,7 +107,7 @@ The available proxy endpoints are: Go's use of proxies is configured with the `GOPROXY` environment variable, as a comma separated list of URLs. Go 1.14 adds support for comma separated list of URLs. Go 1.14 adds support for using `go env -w` to manage Go's environment -variables. For example, `go env -w GOPROXY=...` writes to to `$GOPATH/env` +variables. For example, `go env -w GOPROXY=...` writes to `$GOPATH/env` (which defaults to `~/.go/env`). `GOPROXY` can also be configured as a normal environment variable, with RC files or `export GOPROXY=...`. -- GitLab From 1129e14c043073bce2deec5bc12c2a4a0440ea84 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Wed, 20 May 2020 22:22:31 -0500 Subject: [PATCH 17/19] Bump to 13.1 --- doc/administration/packages/index.md | 2 +- doc/user/packages/index.md | 2 +- ee/lib/api/go_proxy.rb | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index dd8d5e9c6ae455..826d7b2847db9d 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -13,7 +13,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [Conan Repository](../../user/packages/conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ | | [Maven Repository](../../user/packages/maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | | [NPM Registry](../../user/packages/npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | -| [Go Proxy](../../user/packages/go_proxy/index.md) | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | +| [Go Proxy](../../user/packages/go_proxy/index.md) | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.1+ | Don't you see your package management system supported yet? Please consider contributing diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index 6e939d7daca633..341d4ddfa35960 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -21,7 +21,7 @@ The Packages feature allows GitLab to act as a repository for the following: | [NPM Registry](npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | | [NuGet Repository](nuget_repository/index.md) **(PREMIUM)** | The GitLab NuGet Repository will enable every project in GitLab to have its own space to store [NuGet](https://www.nuget.org/) packages. | 12.8+ | | [PyPi Repository](pypi_repository/index.md) **(PREMIUM)** | The GitLab PyPi Repository will enable every project in GitLab to have its own space to store [PyPi](https://pypi.org/) packages. | 12.10+ | -| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.0+ | +| [Go Proxy](go_proxy/index.md) **(PREMIUM)** | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.1+ | ## Enable the Package Registry for your project diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index fd10c1d60ad1ff..f29b0c3dd2077a 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -66,7 +66,7 @@ def find_version namespace ':id/packages/go/*module_name/@v' do desc 'Get all tagged versions for a given Go module' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/list. This feature was introduced in GitLab 13.0.' + detail 'See `go help goproxy`, GET $GOPROXY//@v/list. This feature was introduced in GitLab 13.1.' end get 'list' do mod = find_module @@ -76,7 +76,7 @@ def find_version end desc 'Get information about the given module version' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/.info. This feature was introduced in GitLab 13.0.' + detail 'See `go help goproxy`, GET $GOPROXY//@v/.info. This feature was introduced in GitLab 13.1.' success EE::API::Entities::GoModuleVersion end params do @@ -89,7 +89,7 @@ def find_version end desc 'Get the module file of the given module version' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod. This feature was introduced in GitLab 13.0.' + detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod. This feature was introduced in GitLab 13.1.' end params do requires :module_version, type: String, desc: 'Module version' @@ -102,7 +102,7 @@ def find_version end desc 'Get a zip of the source of the given module version' do - detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip. This feature was introduced in GitLab 13.0.' + detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip. This feature was introduced in GitLab 13.1.' end params do requires :module_version, type: String, desc: 'Module version' -- GitLab From 4b568ff02a44e093cc48321737d161b03897e5b2 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Tue, 19 May 2020 19:50:08 -0500 Subject: [PATCH 18/19] Refactor Go proxy models - Reduce coupling between GoModule and VersionFinder - Do not fetch go.mod when blobs have already been fetched --- ee/app/finders/packages/go/version_finder.rb | 46 +++-------------- ee/app/models/packages/go_module.rb | 53 +++++++++++++++++++- ee/app/models/packages/go_module_version.rb | 35 +++++++------ ee/lib/api/go_proxy.rb | 2 +- 4 files changed, 79 insertions(+), 57 deletions(-) diff --git a/ee/app/finders/packages/go/version_finder.rb b/ee/app/finders/packages/go/version_finder.rb index c8c4778355af10..2b2e278a748b4d 100644 --- a/ee/app/finders/packages/go/version_finder.rb +++ b/ee/app/finders/packages/go/version_finder.rb @@ -14,7 +14,7 @@ def initialize(mod) def execute @mod.project.repository.tags .filter { |tag| semver? tag } - .map { |tag| find_ref tag } + .map { |tag| @mod.version_by(ref: tag) } .filter { |ver| ver.valid? } end @@ -22,55 +22,23 @@ def find(target) case target when String if pseudo_version? target - find_pseudo_version target + semver = parse_semver(target) + commit = pseudo_version_commit(@mod.project, semver) + Packages::GoModuleVersion.new(@mod, :pseudo, commit, name: target, semver: semver) else - mod.versions.find { |v| v.name == target } + @mod.version_by(ref: target) end when Gitlab::Git::Ref - find_ref target + @mod.version_by(ref: target) when ::Commit, Gitlab::Git::Commit - find_commit target + @mod.version_by(commit: target) else raise ArgumentError.new 'not a valid target' end end - - private - - def find_ref(ref) - commit = ref.dereferenced_target - Packages::GoModuleVersion.new(@mod, :ref, commit, ref: ref, semver: parse_semver(ref.name)) - end - - def find_commit(commit) - Packages::GoModuleVersion.new(@mod, :commit, commit) - end - - def find_pseudo_version(str) - semver = parse_semver(str) - raise ArgumentError.new 'target is not a pseudo-version' unless pseudo_version?(semver) - - # valid pseudo-versions are - # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X - # vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre - # vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z - - # go discards the timestamp when resolving pseudo-versions, so we will do the same - - timestamp, sha = semver.prerelease.split('-').last 2 - timestamp = timestamp.split('.').last - commit = @mod.project.repository.commit_by(oid: sha) - - # these errors are copied from proxy.golang.org's responses - raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless commit - raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12 - raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp - - Packages::GoModuleVersion.new(@mod, :pseudo, commit, name: str, semver: semver) - end end end end diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb index 9be705d7a39f06..2cfd927f545ae1 100644 --- a/ee/app/models/packages/go_module.rb +++ b/ee/app/models/packages/go_module.rb @@ -13,8 +13,19 @@ def versions @versions ||= Packages::Go::VersionFinder.new(self).execute end - def find_version(name) - Packages::Go::VersionFinder.new(self).find(name) + def version_by(ref: nil, commit: nil) + raise ArgumentError.new 'no filter specified' unless ref || commit + raise ArgumentError.new 'ref and commit are mutually exclusive' if ref && commit + + if commit + return version_by_sha(commit) if commit.is_a? String + + return version_by_commit(commit) + end + + return version_by_name(ref) if ref.is_a? String + + version_by_ref(ref) end def path_valid?(major) @@ -31,4 +42,42 @@ def path_valid?(major) def gomod_valid?(gomod) gomod&.split("\n", 2)&.first == "module #{@name}" end + + private + + def version_by_name(name) + # avoid a Gitaly call if possible + if defined?(@versions) + v = @versions.find { |v| v.name == ref } + return v if v + end + + ref = @project.repository.find_tag(name) || @project.repository.find_branch(name) + return unless ref + + version_by_ref(ref) + end + + def version_by_ref(ref) + # reuse existing versions + if defined?(@versions) + v = @versions.find { |v| v.ref == ref } + return v if v + end + + commit = ref.dereferenced_target + semver = Packages::SemVer.parse(ref.name, prefixed: true) + Packages::GoModuleVersion.new(self, :ref, commit, ref: ref, semver: semver) + end + + def version_by_sha(sha) + commit = @project.commit_by(oid: sha) + return unless ref + + version_by_commit(commit) + end + + def version_by_commit(commit) + Packages::GoModuleVersion.new(self, :commit, commit) + end end diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index 3bfc40ff3e6ad4..ac300b35251064 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -42,7 +42,14 @@ def full_name end def gomod - @gomod ||= blob_at(@mod.path + '/go.mod') + @gomod ||= + if defined?(@blobs) + blob_at(@mod.path + '/go.mod') + elsif @mod.path.empty? + @mod.project.repository.blob_at(@commit.sha, 'go.mod')&.data + else + @mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data + end end def archive @@ -56,15 +63,12 @@ def archive end end - def files - return @files if defined?(@files) - - sha = @commit.sha - tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } - nested = tree.filter { |e| e.name == 'go.mod' && !(@mod.path == '' && e.path == 'go.mod' || e.path == @mod.path + '/go.mod') }.map { |e| e.path[0..-7] } - @files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } + def valid? + @mod.path_valid?(major) && @mod.gomod_valid?(gomod) end + private + def blob_at(path) return if path.nil? || path.empty? @@ -73,15 +77,16 @@ def blob_at(path) blobs.find { |x| x.path == path }&.data end - def valid? - @mod.path_valid?(major) && @mod.gomod_valid?(gomod) + def blobs + @blobs ||= @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x.path] }) end - private - - def blobs - return @blobs if defined?(@blobs) + def files + return @files if defined?(@files) - @blobs = @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x.path] }) + sha = @commit.sha + tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } + nested = tree.filter { |e| e.name == 'go.mod' && !(@mod.path == '' && e.path == 'go.mod' || e.path == @mod.path + '/go.mod') }.map { |e| e.path[0..-7] } + @files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } end end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index f29b0c3dd2077a..b2d19dab36ba31 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -42,7 +42,7 @@ def find_module def find_version module_version = case_decode params[:module_version] - ver = find_module.find_version(module_version) + ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version) not_found! unless ver&.valid? -- GitLab From d5e63a6d88efe68291a7cf3512ba4a210826e6d4 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Fri, 15 May 2020 19:26:29 -0500 Subject: [PATCH 19/19] Clean up Go proxy implementation - Various fixes and improvements - Hide proxy behind a feature flag, due to performance issues: #218083 - Add a feature flag for testing to disable strict go.mod validation - Document why case en/decoding is necessary - Refactor pseudo-version processing - Move logic from VersionFinder to ModuleHelpers - Document reasoning for matching and validation - Replace BasicAuthHelper and custom method with route setting - Use serach_files_by_name instead of tree to improve performance - Use correct content type for zip: closes #214876 --- doc/user/packages/go_proxy/index.md | 1 - ee/app/finders/packages/go/module_finder.rb | 20 +++---- ee/app/models/packages/go_module.rb | 16 ++++-- ee/app/models/packages/go_module_version.rb | 42 +++++++++++---- ee/app/models/packages/sem_ver.rb | 13 ++--- ee/lib/api/go_proxy.rb | 45 ++++++++-------- .../api/helpers/packages/go/module_helpers.rb | 52 ++++++++++++++++++- ee/spec/factories/go_module_commits.rb | 30 ++++++----- .../finders/packages/go/module_finder_spec.rb | 7 --- .../packages/go/version_finder_spec.rb | 25 ++++++++- ee/spec/models/packages/go_module_spec.rb | 4 ++ .../models/packages/go_module_version_spec.rb | 2 +- ee/spec/requests/api/go_proxy_spec.rb | 27 +++++++++- 13 files changed, 202 insertions(+), 82 deletions(-) diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index eaa0d38dbf6126..a8a803f733cca2 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -56,7 +56,6 @@ Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213770) for de ### Fetch modules from private projects -NOTE: **Note:** `go` does not support transmitting credentials over insecure connections. The steps below work only if GitLab is configured for HTTPS. diff --git a/ee/app/finders/packages/go/module_finder.rb b/ee/app/finders/packages/go/module_finder.rb index 7b18a414552a51..24b145a2d4ba70 100644 --- a/ee/app/finders/packages/go/module_finder.rb +++ b/ee/app/finders/packages/go/module_finder.rb @@ -5,29 +5,31 @@ module Go class ModuleFinder include ::API::Helpers::Packages::Go::ModuleHelpers - GITLAB_GO_URL = (Settings.build_gitlab_go_url + '/').freeze - attr_reader :project, :module_name def initialize(project, module_name) - module_name = CGI.unescape(module_name) module_name = Pathname.new(module_name).cleanpath.to_s @project = project @module_name = module_name end - # rubocop: disable CodeReuse/ActiveRecord def execute - return if @module_name.blank? || !@module_name.start_with?(GITLAB_GO_URL) + return if @module_name.blank? || !@module_name.start_with?(gitlab_go_url) - module_path = @module_name[GITLAB_GO_URL.length..].split('/') + module_path = @module_name[gitlab_go_url.length..].split('/') project_path = project.full_path.split('/') - return unless module_path.take(project_path.length) == project_path + module_project_path = module_path.shift(project_path.length) + return unless module_project_path == project_path + + Packages::GoModule.new(@project, @module_name, module_path.join('/')) + end + + private - Packages::GoModule.new(@project, @module_name, module_path.drop(project_path.length).join('/')) + def gitlab_go_url + @gitlab_go_url ||= Settings.build_gitlab_go_url + '/' end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/ee/app/models/packages/go_module.rb b/ee/app/models/packages/go_module.rb index 2cfd927f545ae1..7229cc70af8285 100644 --- a/ee/app/models/packages/go_module.rb +++ b/ee/app/models/packages/go_module.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Packages::GoModule + include Gitlab::Utils::StrongMemoize + attr_reader :project, :name, :path def initialize(project, name, path) @@ -10,7 +12,7 @@ def initialize(project, name, path) end def versions - @versions ||= Packages::Go::VersionFinder.new(self).execute + strong_memoize(:versions) { Packages::Go::VersionFinder.new(self).execute } end def version_by(ref: nil, commit: nil) @@ -40,6 +42,10 @@ def path_valid?(major) end def gomod_valid?(gomod) + if Feature.enabled?(:go_proxy_disable_gomod_validation, @project) + return gomod&.start_with?("module ") + end + gomod&.split("\n", 2)&.first == "module #{@name}" end @@ -47,8 +53,8 @@ def gomod_valid?(gomod) def version_by_name(name) # avoid a Gitaly call if possible - if defined?(@versions) - v = @versions.find { |v| v.name == ref } + if strong_memoized?(:versions) + v = versions.find { |v| v.name == ref } return v if v end @@ -60,8 +66,8 @@ def version_by_name(name) def version_by_ref(ref) # reuse existing versions - if defined?(@versions) - v = @versions.find { |v| v.ref == ref } + if strong_memoized?(:versions) + v = versions.find { |v| v.ref == ref } return v if v end diff --git a/ee/app/models/packages/go_module_version.rb b/ee/app/models/packages/go_module_version.rb index ac300b35251064..b5c8655358983e 100644 --- a/ee/app/models/packages/go_module_version.rb +++ b/ee/app/models/packages/go_module_version.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Packages::GoModuleVersion + include Gitlab::Utils::StrongMemoize include ::API::Helpers::Packages::Go::ModuleHelpers VALID_TYPES = %i[ref commit pseudo].freeze @@ -42,14 +43,15 @@ def full_name end def gomod - @gomod ||= - if defined?(@blobs) + strong_memoize(:gomod) do + if strong_memoized?(:blobs) blob_at(@mod.path + '/go.mod') elsif @mod.path.empty? @mod.project.repository.blob_at(@commit.sha, 'go.mod')&.data else @mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data end + end end def archive @@ -57,12 +59,26 @@ def archive Zip::OutputStream.write_buffer do |zip| files.each do |file| - zip.put_next_entry "#{full_name}/#{file.path[suffix_len...]}" - zip.write blob_at(file.path) + zip.put_next_entry "#{full_name}/#{file[suffix_len...]}" + zip.write blob_at(file) end end end + def files + strong_memoize(:files) do + ls_tree.filter { |e| !excluded.any? { |n| e.start_with? n } } + end + end + + def excluded + strong_memoize(:excluded) do + ls_tree + .filter { |f| f.end_with?('/go.mod') && f != @mod.path + '/go.mod' } + .map { |f| f[0..-7] } + end + end + def valid? @mod.path_valid?(major) && @mod.gomod_valid?(gomod) end @@ -78,15 +94,19 @@ def blob_at(path) end def blobs - @blobs ||= @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x.path] }) + strong_memoize(:blobs) { @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x] }) } end - def files - return @files if defined?(@files) + def ls_tree + strong_memoize(:ls_tree) do + path = + if @mod.path.empty? + '.' + else + @mod.path + end - sha = @commit.sha - tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } - nested = tree.filter { |e| e.name == 'go.mod' && !(@mod.path == '' && e.path == 'go.mod' || e.path == @mod.path + '/go.mod') }.map { |e| e.path[0..-7] } - @files = tree.filter { |e| !nested.any? { |n| e.path.start_with? n } } + @mod.project.repository.gitaly_repository_client.search_files_by_name(@commit.sha, path) + end end end diff --git a/ee/app/models/packages/sem_ver.rb b/ee/app/models/packages/sem_ver.rb index 760590f3c5a175..b73d51b08b7739 100644 --- a/ee/app/models/packages/sem_ver.rb +++ b/ee/app/models/packages/sem_ver.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Packages::SemVer - # basic semver, but bounded (^expr$) - PATTERN = /\A(v?)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?\z/i.freeze - attr_accessor :major, :minor, :patch, :prerelease, :build def initialize(major = 0, minor = 0, patch = 0, prerelease = nil, build = nil, prefixed: false) @@ -37,11 +34,11 @@ def to_s end def self.match(str, prefixed: false) - m = PATTERN.match(str) - return unless m - return if prefixed == m[1].empty? + return unless str&.start_with?('v') == prefixed + + str = str[1..] if prefixed - m + Gitlab::Regex.semver_regex.match(str) end def self.match?(str, prefixed: false) @@ -52,6 +49,6 @@ def self.parse(str, prefixed: false) m = match str, prefixed: prefixed return unless m - new(m[2].to_i, m[3].to_i, m[4].to_i, m[5], m[6], prefixed: prefixed) + new(m[1].to_i, m[2].to_i, m[3].to_i, m[4], m[5], prefixed: prefixed) end end diff --git a/ee/lib/api/go_proxy.rb b/ee/lib/api/go_proxy.rb index b2d19dab36ba31..fce8f85d098182 100755 --- a/ee/lib/api/go_proxy.rb +++ b/ee/lib/api/go_proxy.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module API class GoProxy < Grape::API - helpers ::API::Helpers::PackagesManagerClientsHelpers - helpers ::API::Helpers::Packages::BasicAuthHelpers + helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::Packages::Go::ModuleHelpers # basic semver, except case encoded (A => !a) @@ -13,27 +12,27 @@ class GoProxy < Grape::API before { require_packages_enabled! } helpers do - # support personal access tokens for HTTP Basic in addition to the usual methods - def find_personal_access_token - pa = find_personal_access_token_from_http_basic_auth - return pa if pa - - # copied from Gitlab::Auth::AuthFinders - token = - current_request.params[::Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_PARAM].presence || - current_request.env[::Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER].presence || - parsed_oauth_token - return unless token - - # Expiration, revocation and scopes are verified in `validate_access_token!` - PersonalAccessToken.find_by_token(token) || raise(::Gitlab::Auth::UnauthorizedError) + def find_project!(id) + # based on API::Helpers::Packages::BasicAuthHelpers#authorized_project_find! + + project = find_project(id) + + return project if project && can?(current_user, :read_project, project) + + if current_user + not_found!('Project') + else + unauthorized! + end end def find_module + not_found! unless Feature.enabled?(:go_proxy, user_project) + module_name = case_decode params[:module_name] bad_request!('Module Name') if module_name.blank? - mod = ::Packages::Go::ModuleFinder.new(authorized_user_project, module_name).execute + mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute not_found! unless mod @@ -55,13 +54,13 @@ def find_version params do requires :id, type: String, desc: 'The ID of a project' - requires :module_name, type: String, desc: 'Module name' + requires :module_name, type: String, desc: 'Module name', coerce_with: ->(val) { CGI.unescape(val) } end - route_setting :authentication, job_token_allowed: true + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before do - authorize_read_package!(authorized_user_project) - authorize_packages_feature!(authorized_user_project) + authorize_read_package! + authorize_packages_feature! end namespace ':id/packages/go/*module_name/@v' do @@ -110,10 +109,10 @@ def find_version get ':module_version.zip', requirements: MODULE_VERSION_REQUIREMENTS do ver = find_version - # TODO: Content-Type should be application/zip, see #214876 + content_type 'application/zip' + env['api.format'] = :binary header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: ver.name + '.zip') header['Content-Transfer-Encoding'] = 'binary' - content_type 'text/plain' status :ok body ver.archive.string end diff --git a/ee/lib/api/helpers/packages/go/module_helpers.rb b/ee/lib/api/helpers/packages/go/module_helpers.rb index b707feda763f95..e79f27388c6c22 100644 --- a/ee/lib/api/helpers/packages/go/module_helpers.rb +++ b/ee/lib/api/helpers/packages/go/module_helpers.rb @@ -6,10 +6,24 @@ module Packages module Go module ModuleHelpers def case_encode(str) + # Converts "github.com/Azure" to "github.com/!azure" + # + # From `go help goproxy`: + # + # > To avoid problems when serving from case-sensitive file systems, + # > the and elements are case-encoded, replacing + # > every uppercase letter with an exclamation mark followed by the + # > corresponding lower-case letter: github.com/Azure encodes as + # > github.com/!azure. + str.gsub(/A-Z/) { |s| "!#{s.downcase}"} end def case_decode(str) + # Converts "github.com/!azure" to "github.com/Azure" + # + # See #case_encode + str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase } end @@ -29,7 +43,7 @@ def pseudo_version?(version) pre = version.prerelease - # valid pseudo-versions are + # Valid pseudo-versions are: # vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X # vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre # vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z @@ -41,7 +55,41 @@ def pseudo_version?(version) pre = pre[m[0].length..] end - /\A\d{14}-[A-Za-z0-9]+\z/.freeze.match? pre + # This pattern is intentionally more forgiving than the patterns + # above. Correctness is verified by #pseudo_version_commit. + /\A\d{14}-\h+\z/.freeze.match? pre + end + + def pseudo_version_commit(project, semver) + # Per Go's implementation of pseudo-versions, a tag should be + # considered a pseudo-version if it matches one of the patterns + # listed in #pseudo_version?, regardless of the content of the + # timestamp or the length of the SHA fragment. However, an error + # should be returned if the timestamp is not correct or if the SHA + # fragment is not exactly 12 characters long. See also Go's + # implementation of: + # + # - [*codeRepo.validatePseudoVersion](https://github.com/golang/go/blob/daf70d6c1688a1ba1699c933b3c3f04d6f2f73d9/src/cmd/go/internal/modfetch/coderepo.go#L530) + # - [Pseudo-version parsing](https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/pseudo.go) + # - [Pseudo-version request processing](https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/coderepo.go) + + # Go ignores anything before '.' or after the second '-', so we will do the same + timestamp, sha = semver.prerelease.split('-').last 2 + timestamp = timestamp.split('.').last + commit = project.repository.commit_by(oid: sha) + + # Error messages are based on the responses of proxy.golang.org + + # Verify that the SHA fragment references a commit + raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless commit + + # Require the SHA fragment to be 12 characters long + raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12 + + # Require the timestamp to match that of the commit + raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp + + commit end def parse_semver(str) diff --git a/ee/spec/factories/go_module_commits.rb b/ee/spec/factories/go_module_commits.rb index aebca73efc04e4..54bbb29da641dc 100644 --- a/ee/spec/factories/go_module_commits.rb +++ b/ee/spec/factories/go_module_commits.rb @@ -75,21 +75,27 @@ transient do name { nil } message { 'Add module' } - end - service do - port = ::Gitlab.config.gitlab.port - host = ::Gitlab.config.gitlab.host - domain = case port when 80, 443 then host else "#{host}:#{port}" end - - url = "#{domain}/#{project.path_with_namespace}" - if name.nil? - path = '' - else - url += '/' + name - path = name + '/' + url do + v = "#{::Gitlab.config.gitlab.host}/#{project.path_with_namespace}" + + if name + v + '/' + name + else + v + end end + path do + if name + name + '/' + else + '' + end + end + end + + service do Files::MultiService.new( project, project.owner, diff --git a/ee/spec/finders/packages/go/module_finder_spec.rb b/ee/spec/finders/packages/go/module_finder_spec.rb index 307f823047bcb0..e296e30e9284d3 100644 --- a/ee/spec/finders/packages/go/module_finder_spec.rb +++ b/ee/spec/finders/packages/go/module_finder_spec.rb @@ -58,13 +58,6 @@ end end - context 'with a URL encoded relative path component' do - it_behaves_like 'an invalid path' do - let(:module_name) { base_url(project) + '/%2E%2E%2Fxyz' } - let(:expected_name) { base_url(project.namespace) + '/xyz' } - end - end - context 'with many relative path components' do it_behaves_like 'an invalid path' do let(:module_name) { base_url(project) + ('/..' * 10) + '/xyz' } diff --git a/ee/spec/finders/packages/go/version_finder_spec.rb b/ee/spec/finders/packages/go/version_finder_spec.rb index 0789091ae7aa0c..76f010cda4b235 100644 --- a/ee/spec/finders/packages/go/version_finder_spec.rb +++ b/ee/spec/finders/packages/go/version_finder_spec.rb @@ -13,11 +13,16 @@ create :go_module_commit, :module, project: project, tag: 'v1.0.1' create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' + create :go_module_commit, :module, project: project, tag: 'v1.0.4', name: 'bad-mod', url: 'example.com/go-lib' create :go_module_commit, :files, project: project, tag: 'c1', files: { 'y.go' => "package a\n" } create :go_module_commit, :module, project: project, tag: 'c2', name: 'v2' create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } end + before do + stub_feature_flags(go_proxy_disable_gomod_validation: false) + end + shared_examples '#execute' do |*expected| it "returns #{expected.empty? ? 'nothing' : expected.join(', ')}" do actual = finder.execute.map { |x| x.name } @@ -35,7 +40,7 @@ context 'for the root module' do let(:mod) { create :go_module, project: project } - it_behaves_like '#execute', 'v1.0.1', 'v1.0.2', 'v1.0.3' + it_behaves_like '#execute', 'v1.0.1', 'v1.0.2', 'v1.0.3', 'v1.0.4' end context 'for the package' do @@ -47,7 +52,7 @@ context 'for the submodule' do let(:mod) { create :go_module, project: project, path: 'mod' } - it_behaves_like '#execute', 'v1.0.3' + it_behaves_like '#execute', 'v1.0.3', 'v1.0.4' end context 'for the root module v2' do @@ -55,6 +60,22 @@ it_behaves_like '#execute', 'v2.0.0' end + + context 'for the bad module' do + let(:mod) { create :go_module, project: project, path: 'bad-mod' } + + context 'with gomod checking enabled' do + it_behaves_like '#execute' + end + + context 'with gomod checking disabled' do + before do + stub_feature_flags(go_proxy_disable_gomod_validation: true) + end + + it_behaves_like '#execute', 'v1.0.4' + end + end end describe '#find' do diff --git a/ee/spec/models/packages/go_module_spec.rb b/ee/spec/models/packages/go_module_spec.rb index 823b41401b99d5..fa7c02861266fb 100644 --- a/ee/spec/models/packages/go_module_spec.rb +++ b/ee/spec/models/packages/go_module_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Packages::GoModule, type: :model do + before do + stub_feature_flags(go_proxy_disable_gomod_validation: false) + end + describe '#path_valid?' do context 'with root path' do let_it_be(:package) { create(:go_module) } diff --git a/ee/spec/models/packages/go_module_version_spec.rb b/ee/spec/models/packages/go_module_version_spec.rb index d76d924201f7e9..f4643187f7f2a4 100644 --- a/ee/spec/models/packages/go_module_version_spec.rb +++ b/ee/spec/models/packages/go_module_version_spec.rb @@ -19,7 +19,7 @@ shared_examples '#files' do |desc, *entries| it "returns #{desc}" do - actual = version.files.map { |x| x.path }.to_set + actual = version.files.map { |x| x }.to_set expect(actual).to eq(entries.to_set) end end diff --git a/ee/spec/requests/api/go_proxy_spec.rb b/ee/spec/requests/api/go_proxy_spec.rb index b77caea8f616df..77e9a1dee08fe9 100644 --- a/ee/spec/requests/api/go_proxy_spec.rb +++ b/ee/spec/requests/api/go_proxy_spec.rb @@ -29,7 +29,9 @@ before do project.add_developer(user) + stub_licensed_features(packages: true) + stub_feature_flags(go_proxy_disable_gomod_validation: false) modules end @@ -54,7 +56,7 @@ end end - shared_examples 'a missing module version list resource' do |*versions, path: ''| + shared_examples 'a missing module version list resource' do |path: ''| let(:module_name) { "#{base}#{path}" } let(:resource) { "list" } @@ -168,6 +170,18 @@ context 'for the root module v2' do it_behaves_like 'a module version list resource', 'v2.0.0', path: '/v2' end + + context 'with a URL encoded relative path component' do + it_behaves_like 'a missing module version list resource', path: '/%2E%2E%2Fxyz' + end + + context 'with the feature disabled' do + before do + stub_feature_flags(go_proxy: false) + end + + it_behaves_like 'a missing module version list resource' + end end describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do @@ -356,26 +370,31 @@ it 'returns ok with an oauth token' do get_resource(oauth_access_token: oauth) + expect(response).to have_gitlab_http_status(:ok) end it 'returns ok with a job token' do get_resource(oauth_access_token: job) + expect(response).to have_gitlab_http_status(:ok) end it 'returns ok with a personal access token' do get_resource(personal_access_token: pa_token) + expect(response).to have_gitlab_http_status(:ok) end it 'returns ok with a personal access token and basic authentication' do get_resource(headers: build_basic_auth_header(user.username, pa_token.token)) + expect(response).to have_gitlab_http_status(:ok) end it 'returns unauthorized with no authentication' do get_resource + expect(response).to have_gitlab_http_status(:unauthorized) end end @@ -393,6 +412,7 @@ it 'returns ok with no authentication' do get_resource + expect(response).to have_gitlab_http_status(:ok) end end @@ -406,26 +426,31 @@ def get_resource(user = nil, **params) describe 'GET /projects/:id/packages/go/*module_name/@v/list' do it 'returns not found with a user' do get_resource(user) + expect(response).to have_gitlab_http_status(:not_found) end it 'returns not found with an oauth token' do get_resource(oauth_access_token: oauth) + expect(response).to have_gitlab_http_status(:not_found) end it 'returns not found with a job token' do get_resource(oauth_access_token: job) + expect(response).to have_gitlab_http_status(:not_found) end it 'returns not found with a personal access token' do get_resource(personal_access_token: pa_token) + expect(response).to have_gitlab_http_status(:not_found) end it 'returns unauthorized with no authentication' do get_resource + expect(response).to have_gitlab_http_status(:unauthorized) end end -- GitLab