Commit 95a3e1a1 authored by Ethan Reesor's avatar Ethan Reesor

Implement Go module proxy pseudo-versions

parent 575b8495
# 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 @@ class Packages::GoModule
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
......
......@@ -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 @@ class Packages::GoModuleVersion
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
......@@ -12,7 +12,7 @@ module Packages
end
def time
@version.tag.dereferenced_target.committed_date
@version.commit.committed_date
end
end
end
......
......@@ -28,7 +28,7 @@ module API
mod = find_module
ver = mod.find_version case_decode params[:module_version]
not_found! unless ver
not_found! unless ver&.valid?
ver
end
......
......@@ -23,11 +23,11 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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 @@ describe API::GoProxy do
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)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment