From 4594506d7c20c3c5505e5a65372d72aa9b7a8c2d Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 22 Aug 2022 11:24:15 +0000 Subject: [PATCH 1/7] Add support for Vagrant boxes. --- models/packages/descriptor.go | 3 + models/packages/package.go | 5 + modules/packages/vagrant/metadata.go | 92 ++++++++++ routers/api/packages/api.go | 11 ++ routers/api/packages/vagrant/vagrant.go | 223 ++++++++++++++++++++++++ routers/api/v1/packages/package.go | 2 +- templates/admin/packages/list.tmpl | 1 + templates/package/content/vagrant.tmpl | 14 ++ templates/package/metadata/vagrant.tmpl | 5 + templates/package/shared/list.tmpl | 1 + templates/package/view.tmpl | 2 + templates/swagger/v1_json.tmpl | 3 +- 12 files changed, 360 insertions(+), 2 deletions(-) create mode 100644 modules/packages/vagrant/metadata.go create mode 100644 routers/api/packages/vagrant/vagrant.go create mode 100644 templates/package/content/vagrant.tmpl create mode 100644 templates/package/metadata/vagrant.tmpl diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index dc753421d0210..357574a706c2d 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/packages/pub" "code.gitea.io/gitea/modules/packages/pypi" "code.gitea.io/gitea/modules/packages/rubygems" + "code.gitea.io/gitea/modules/packages/vagrant" "github.com/hashicorp/go-version" ) @@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &pypi.Metadata{} case TypeRubyGems: metadata = &rubygems.Metadata{} + case TypeVagrant: + metadata = &vagrant.Metadata{} default: panic(fmt.Sprintf("unknown package type: %s", string(p.Type))) } diff --git a/models/packages/package.go b/models/packages/package.go index 39b1c83cfabf6..c203901a2d4c8 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -42,6 +42,7 @@ const ( TypePub Type = "pub" TypePyPI Type = "pypi" TypeRubyGems Type = "rubygems" + TypeVagrant Type = "vagrant" ) // Name gets the name of the package type @@ -69,6 +70,8 @@ func (pt Type) Name() string { return "PyPI" case TypeRubyGems: return "RubyGems" + case TypeVagrant: + return "Vagrant" } panic(fmt.Sprintf("unknown package type: %s", string(pt))) } @@ -98,6 +101,8 @@ func (pt Type) SVGName() string { return "gitea-python" case TypeRubyGems: return "gitea-rubygems" + case TypeVagrant: + return "gitea-vagrant" } panic(fmt.Sprintf("unknown package type: %s", string(pt))) } diff --git a/modules/packages/vagrant/metadata.go b/modules/packages/vagrant/metadata.go new file mode 100644 index 0000000000000..ff8f4abd705eb --- /dev/null +++ b/modules/packages/vagrant/metadata.go @@ -0,0 +1,92 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package vagrant + +import ( + "archive/tar" + "compress/gzip" + "io" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/validation" +) + +const ( + PropertyProvider = "vagrant.provider" +) + +// Metadata represents the metadata of a Vagrant package +type Metadata struct { + Author string `json:"author,omitempty"` + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` +} + +// ParseMetadataFromBox parses the metdata of a box file +func ParseMetadataFromBox(r io.Reader) (*Metadata, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + if hd.Name == "info.json" { + return ParseInfoFile(tr) + } + } + + return &Metadata{}, nil +} + +// ParseInfoFile parses a info.json file to retrieve the metadata of a Vagrant package +func ParseInfoFile(r io.Reader) (*Metadata, error) { + var values map[string]string + if err := json.NewDecoder(r).Decode(&values); err != nil { + return nil, err + } + + m := &Metadata{} + + // There is no defined format for this file, just try the common keys + for k, v := range values { + switch strings.ToLower(k) { + case "description": + case "short_description": + m.Description = v + case "website": + case "homepage": + case "url": + if validation.IsValidURL(v) { + m.ProjectURL = v + } + case "repository": + case "source": + if validation.IsValidURL(v) { + m.RepositoryURL = v + } + case "author": + case "authors": + m.Author = v + } + } + + return m, nil +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index cbf041a7e136c..9e03e2d1836ba 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/routers/api/packages/pub" "code.gitea.io/gitea/routers/api/packages/pypi" "code.gitea.io/gitea/routers/api/packages/rubygems" + "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" ) @@ -265,6 +266,16 @@ func Routes() *web.Route { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }) + r.Group("/vagrant", func() { + r.Group("/{name}", func() { + r.Head("", vagrant.CheckBoxAvailable) + r.Get("", vagrant.EnumeratePackageVersions) + r.Group("/{version}/{provider}", func() { + r.Get("", vagrant.DownloadPackageFile) + r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile) + }) + }) + }) }, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) return r diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go new file mode 100644 index 0000000000000..56a548a401d1c --- /dev/null +++ b/routers/api/packages/vagrant/vagrant.go @@ -0,0 +1,223 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package vagrant + +import ( + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + vagrant_module "code.gitea.io/gitea/modules/packages/vagrant" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/hashicorp/go-version" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.Status(status) + }) +} + +func CheckBoxAvailable(ctx *context.Context) { + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name")) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, err) + return + } + + ctx.JSON(http.StatusOK, nil) // needs to be Content-Type: application/json +} + +type packageMetadata struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + ShortDescription string `json:"short_description,omitempty"` + Versions []*versionMetadata `json:"versions"` +} + +type versionMetadata struct { + Version string `json:"version"` + Status string `json:"status"` + DescriptionHTML string `json:"description_html,omitempty"` + DescriptionMarkdown string `json:"description_markdown,omitempty"` + Providers []*providerData `json:"providers"` +} + +type providerData struct { + Name string `json:"name"` + URL string `json:"url"` + Checksum string `json:"checksum"` + ChecksumType string `json:"checksum_type"` +} + +func packageDescriptorToMetadata(baseURL string, pd *packages_model.PackageDescriptor) *versionMetadata { + versionURL := baseURL + "/" + url.PathEscape(pd.Version.Version) + + providers := make([]*providerData, 0, len(pd.Files)) + + for _, f := range pd.Files { + providers = append(providers, &providerData{ + Name: f.Properties.GetByName(vagrant_module.PropertyProvider), + URL: versionURL + "/" + url.PathEscape(f.File.Name), + Checksum: f.Blob.HashSHA512, + ChecksumType: "sha512", + }) + } + + return &versionMetadata{ + Status: "active", + Version: pd.Version.Version, + Providers: providers, + } +} + +func EnumeratePackageVersions(ctx *context.Context) { + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name")) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + sort.Slice(pds, func(i, j int) bool { + return pds[i].SemVer.LessThan(pds[j].SemVer) + }) + + baseURL := fmt.Sprintf("%sapi/packages/%s/vagrant/%s", setting.AppURL, url.PathEscape(ctx.Package.Owner.Name), url.PathEscape(pds[0].Package.Name)) + + versions := make([]*versionMetadata, 0, len(pds)) + for _, pd := range pds { + versions = append(versions, packageDescriptorToMetadata(baseURL, pd)) + } + + ctx.JSON(http.StatusOK, &packageMetadata{ + Name: pds[0].Package.Name, + Versions: versions, + }) +} + +func UploadPackageFile(ctx *context.Context) { + boxName := ctx.Params("name") + boxVersion := ctx.Params("version") + _, err := version.NewSemver(boxVersion) + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + boxProvider := ctx.Params("provider") + if !strings.HasSuffix(boxProvider, ".box") { + apiError(ctx, http.StatusBadRequest, err) + return + } + + upload, needsClose, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if needsClose { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + metadata, err := vagrant_module.ParseMetadataFromBox(buf) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeVagrant, + Name: boxName, + Version: boxVersion, + }, + SemverCompatible: true, + Creator: ctx.Doer, + Metadata: metadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: strings.ToLower(boxProvider), + }, + Data: buf, + IsLead: true, + Properties: map[string]string{ + vagrant_module.PropertyProvider: strings.TrimSuffix(boxProvider, ".box"), + }, + }, + ) + if err != nil { + if err == packages_model.ErrDuplicatePackageVersion { + apiError(ctx, http.StatusBadRequest, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) +} + +func DownloadPackageFile(ctx *context.Context) { + s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeVagrant, + Name: ctx.Params("name"), + Version: ctx.Params("version"), + }, + &packages_service.PackageFileInfo{ + Filename: ctx.Params("provider"), + }, + ) + if err != nil { + if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 2c023891022aa..29fa6c85a5b6f 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -41,7 +41,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems] + // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] // - name: q // in: query // description: name filter diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 61721532a441b..06d6163476a9f 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -24,6 +24,7 @@ + diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl new file mode 100644 index 0000000000000..efa2939515f4e --- /dev/null +++ b/templates/package/content/vagrant.tmpl @@ -0,0 +1,14 @@ +{{if eq .PackageDescriptor.Package.Type "vagrant"}} +

{{.locale.Tr "packages.installation"}}

+
+
+
+ +
vagrant box add "{{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"
+
+
+ +
+
+
+{{end}} diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl new file mode 100644 index 0000000000000..344e417b77f99 --- /dev/null +++ b/templates/package/metadata/vagrant.tmpl @@ -0,0 +1,5 @@ +{{if eq .PackageDescriptor.Package.Type "vagrant"}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.conan.details.repository"}}
{{end}} +{{end}} diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 020acba9d73a5..b48f772f39fd2 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -17,6 +17,7 @@ + diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 0116da53b3517..a6c72e8f692e2 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -30,6 +30,7 @@ {{template "package/content/pub" .}} {{template "package/content/pypi" .}} {{template "package/content/rubygems" .}} + {{template "package/content/vagrant" .}}
@@ -52,6 +53,7 @@ {{template "package/metadata/pub" .}} {{template "package/metadata/pypi" .}} {{template "package/metadata/rubygems" .}} + {{template "package/metadata/vagrant" .}}
{{if not (eq .PackageDescriptor.Package.Type "container")}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 19f16b5c1c39a..1bc5eaf1d917f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1962,7 +1962,8 @@ "nuget", "pub", "pypi", - "rubygems" + "rubygems", + "vagrant" ], "type": "string", "description": "package type filter", From daee525f9a763213c243f8413d051735e00a6906 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 22 Aug 2022 12:55:40 +0000 Subject: [PATCH 2/7] Add authentication. --- routers/api/packages/api.go | 4 ++++ routers/api/packages/vagrant/vagrant.go | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 9e03e2d1836ba..cdde643c8db33 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -267,6 +267,10 @@ func Routes() *web.Route { }, reqPackageAccess(perm.AccessModeWrite)) }) r.Group("/vagrant", func() { + r.Group("/authenticate", func() { + //r.Post("", vagrant.Authenticate) + r.Get("", vagrant.CheckAuthenticate) + }) r.Group("/{name}", func() { r.Head("", vagrant.CheckBoxAvailable) r.Get("", vagrant.EnumeratePackageVersions) diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 56a548a401d1c..7705b7c5bbef4 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -25,10 +25,25 @@ import ( func apiError(ctx *context.Context, status int, obj interface{}) { helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.Status(status) + ctx.JSON(status, struct { + Errors []string `json:"errors"` + }{ + Errors: []string{ + message, + }, + }) }) } +func CheckAuthenticate(ctx *context.Context) { + if ctx.Doer == nil { + apiError(ctx, http.StatusUnauthorized, "Invalid access token") + return + } + + ctx.Status(http.StatusOK) +} + func CheckBoxAvailable(ctx *context.Context) { pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name")) if err != nil { From 72b747c2b7196f0ce3275a5cb37e015aacf1bf4d Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 22 Aug 2022 19:38:31 +0000 Subject: [PATCH 3/7] Add tests. --- modules/packages/vagrant/metadata.go | 5 + modules/packages/vagrant/metadata_test.go | 111 ++++++++++++++++++++++ routers/api/packages/api.go | 2 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 modules/packages/vagrant/metadata_test.go diff --git a/modules/packages/vagrant/metadata.go b/modules/packages/vagrant/metadata.go index ff8f4abd705eb..278dfab32e813 100644 --- a/modules/packages/vagrant/metadata.go +++ b/modules/packages/vagrant/metadata.go @@ -69,20 +69,25 @@ func ParseInfoFile(r io.Reader) (*Metadata, error) { for k, v := range values { switch strings.ToLower(k) { case "description": + fallthrough case "short_description": m.Description = v case "website": + fallthrough case "homepage": + fallthrough case "url": if validation.IsValidURL(v) { m.ProjectURL = v } case "repository": + fallthrough case "source": if validation.IsValidURL(v) { m.RepositoryURL = v } case "author": + fallthrough case "authors": m.Author = v } diff --git a/modules/packages/vagrant/metadata_test.go b/modules/packages/vagrant/metadata_test.go new file mode 100644 index 0000000000000..9720c945ae8ab --- /dev/null +++ b/modules/packages/vagrant/metadata_test.go @@ -0,0 +1,111 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package vagrant + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "code.gitea.io/gitea/modules/json" + + "github.com/stretchr/testify/assert" +) + +const ( + author = "gitea" + description = "Package Description" + projectURL = "https://gitea.io" + repositoryURL = "https://gitea.io/gitea/gitea" +) + +func TestParseMetadataFromBox(t *testing.T) { + createArchive := func(files map[string][]byte) io.Reader { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + for filename, content := range files { + hdr := &tar.Header{ + Name: filename, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + } + tw.Close() + zw.Close() + return &buf + } + + t.Run("MissingInfoFile", func(t *testing.T) { + data := createArchive(map[string][]byte{"dummy.txt": {}}) + + metadata, err := ParseMetadataFromBox(data) + assert.NotNil(t, metadata) + assert.NoError(t, err) + }) + + t.Run("Valid", func(t *testing.T) { + content, err := json.Marshal(map[string]string{ + "description": description, + "author": author, + "website": projectURL, + "repository": repositoryURL, + }) + assert.NoError(t, err) + + data := createArchive(map[string][]byte{"info.json": content}) + + metadata, err := ParseMetadataFromBox(data) + assert.NotNil(t, metadata) + assert.NoError(t, err) + + assert.Equal(t, author, metadata.Author) + assert.Equal(t, description, metadata.Description) + assert.Equal(t, projectURL, metadata.ProjectURL) + assert.Equal(t, repositoryURL, metadata.RepositoryURL) + }) +} + +func TestParseInfoFile(t *testing.T) { + t.Run("UnknownKeys", func(t *testing.T) { + content, err := json.Marshal(map[string]string{ + "package": "", + "dummy": "", + }) + assert.NoError(t, err) + + metadata, err := ParseInfoFile(bytes.NewReader(content)) + assert.NotNil(t, metadata) + assert.NoError(t, err) + + assert.Empty(t, metadata.Author) + assert.Empty(t, metadata.Description) + assert.Empty(t, metadata.ProjectURL) + assert.Empty(t, metadata.RepositoryURL) + }) + + t.Run("Valid", func(t *testing.T) { + content, err := json.Marshal(map[string]string{ + "description": description, + "author": author, + "website": projectURL, + "repository": repositoryURL, + }) + assert.NoError(t, err) + + metadata, err := ParseInfoFile(bytes.NewReader(content)) + assert.NotNil(t, metadata) + assert.NoError(t, err) + + assert.Equal(t, author, metadata.Author) + assert.Equal(t, description, metadata.Description) + assert.Equal(t, projectURL, metadata.ProjectURL) + assert.Equal(t, repositoryURL, metadata.RepositoryURL) + }) +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index cdde643c8db33..6d096371e9303 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -268,7 +268,7 @@ func Routes() *web.Route { }) r.Group("/vagrant", func() { r.Group("/authenticate", func() { - //r.Post("", vagrant.Authenticate) + // r.Post("", vagrant.Authenticate) r.Get("", vagrant.CheckAuthenticate) }) r.Group("/{name}", func() { From fecbbc26cec324d452ea0f1bb5d9e07f88b30506 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 22 Aug 2022 21:44:10 +0000 Subject: [PATCH 4/7] Add integration tests. --- integrations/api_packages_vagrant_test.go | 170 ++++++++++++++++++++++ options/locale/locale_en-US.ini | 2 + routers/api/packages/vagrant/vagrant.go | 9 +- templates/package/content/vagrant.tmpl | 8 +- 4 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 integrations/api_packages_vagrant_test.go diff --git a/integrations/api_packages_vagrant_test.go b/integrations/api_packages_vagrant_test.go new file mode 100644 index 0000000000000..8ce4f24ef5cc4 --- /dev/null +++ b/integrations/api_packages_vagrant_test.go @@ -0,0 +1,170 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "net/http" + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + vagrant_module "code.gitea.io/gitea/modules/packages/vagrant" + + "github.com/stretchr/testify/assert" +) + +func TestPackageVagrant(t *testing.T) { + defer prepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + token := "Bearer " + getUserToken(t, user.Name) + + packageName := "test_package" + packageVersion := "1.0.1" + packageDescription := "Test Description" + packageProvider := "virtualbox" + + filename := fmt.Sprintf("%s.box", packageProvider) + + infoContent, _ := json.Marshal(map[string]string{ + "description": packageDescription, + }) + + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + archive := tar.NewWriter(zw) + archive.WriteHeader(&tar.Header{ + Name: "info.json", + Mode: 0o600, + Size: int64(len(infoContent)), + }) + archive.Write(infoContent) + archive.Close() + zw.Close() + content := buf.Bytes() + + root := fmt.Sprintf("/api/packages/%s/vagrant", user.Name) + + t.Run("Authenticate", func(t *testing.T) { + defer PrintCurrentTest(t)() + + authenticateURL := fmt.Sprintf("%s/authenticate", root) + + req := NewRequest(t, "GET", authenticateURL) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "GET", authenticateURL) + addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusOK) + }) + + boxURL := fmt.Sprintf("%s/%s", root, packageName) + + t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "HEAD", boxURL) + MakeRequest(t, req, http.StatusNotFound) + + uploadURL := fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "HEAD", boxURL) + resp := MakeRequest(t, req, http.StatusOK) + assert.True(t, strings.HasPrefix(resp.HeaderMap.Get("Content-Type"), "application/json")) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeVagrant) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.NotNil(t, pd.SemVer) + assert.IsType(t, &vagrant_module.Metadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, filename, pfs[0].Name) + assert.True(t, pfs[0].IsLead) + + pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) + assert.NoError(t, err) + assert.Equal(t, int64(len(content)), pb.Size) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusConflict) + }) + + t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, content, resp.Body.Bytes()) + }) + + t.Run("EnumeratePackageVersions", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", boxURL) + resp := MakeRequest(t, req, http.StatusOK) + + type providerData struct { + Name string `json:"name"` + URL string `json:"url"` + Checksum string `json:"checksum"` + ChecksumType string `json:"checksum_type"` + } + + type versionMetadata struct { + Version string `json:"version"` + Status string `json:"status"` + DescriptionHTML string `json:"description_html,omitempty"` + DescriptionMarkdown string `json:"description_markdown,omitempty"` + Providers []*providerData `json:"providers"` + } + + type packageMetadata struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + ShortDescription string `json:"short_description,omitempty"` + Versions []*versionMetadata `json:"versions"` + } + + var result packageMetadata + DecodeJSON(t, resp, &result) + + assert.Equal(t, packageName, result.Name) + assert.Equal(t, packageDescription, result.Description) + assert.Len(t, result.Versions, 1) + version := result.Versions[0] + assert.Equal(t, packageVersion, version.Version) + assert.Equal(t, "active", version.Status) + assert.Len(t, version.Providers, 1) + provider := version.Providers[0] + assert.Equal(t, packageProvider, provider.Name) + assert.Equal(t, "sha512", provider.ChecksumType) + assert.Equal(t, "259bebd6160acad695016d22a45812e26f187aaf78e71a4c23ee3201528346293f991af3468a8c6c5d2a21d7d9e1bdc1bf79b87110b2fddfcc5a0d45963c7c30", provider.Checksum) + }) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0e309279d29b6..fe139bcfc4e79 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3128,6 +3128,8 @@ rubygems.dependencies.development = Development Dependencies rubygems.required.ruby = Requires Ruby version rubygems.required.rubygems = Requires RubyGem version rubygems.documentation = For more information on the RubyGems registry, see the documentation. +vagrant.install = To add a Vagrant box, run the following command: +vagrant.documentation = For more information on the Vagrant registry, see the documentation. settings.link = Link this package to a repository settings.link.description = If you link a package with a repository, the package is listed in the repository's package list. settings.link.select = Select Repository diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 7705b7c5bbef4..1e3fc7c120582 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -130,8 +130,9 @@ func EnumeratePackageVersions(ctx *context.Context) { } ctx.JSON(http.StatusOK, &packageMetadata{ - Name: pds[0].Package.Name, - Versions: versions, + Name: pds[0].Package.Name, + Description: pds[len(pds)-1].Metadata.(*vagrant_module.Metadata).Description, + Versions: versions, }) } @@ -200,8 +201,8 @@ func UploadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrDuplicatePackageVersion { - apiError(ctx, http.StatusBadRequest, err) + if err == packages_model.ErrDuplicatePackageFile { + apiError(ctx, http.StatusConflict, err) return } apiError(ctx, http.StatusInternalServerError, err) diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl index efa2939515f4e..7bd686e6ab689 100644 --- a/templates/package/content/vagrant.tmpl +++ b/templates/package/content/vagrant.tmpl @@ -3,12 +3,16 @@
- -
vagrant box add "{{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"
+ +
vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "{{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"
+ {{if .PackageDescriptor.Metadata.Description}} +

{{.locale.Tr "packages.about"}}

+
{{.PackageDescriptor.Metadata.Description}}
+ {{end}} {{end}} From 5eb7fa7a7fdf707dbf06bb2253d6f9eea71d37af Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 23 Aug 2022 15:04:26 +0000 Subject: [PATCH 5/7] Add docs. --- docs/content/doc/packages/overview.en-us.md | 1 + docs/content/doc/packages/vagrant.en-us.md | 78 +++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/content/doc/packages/vagrant.en-us.md diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md index 5e03b710170f1..0abb054b0f1ba 100644 --- a/docs/content/doc/packages/overview.en-us.md +++ b/docs/content/doc/packages/overview.en-us.md @@ -37,6 +37,7 @@ The following package managers are currently supported: | [Pub]({{< relref "doc/packages/pub.en-us.md" >}}) | Dart | `dart`, `flutter` | | [PyPI]({{< relref "doc/packages/pypi.en-us.md" >}}) | Python | `pip`, `twine` | | [RubyGems]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Ruby | `gem`, `Bundler` | +| [Vagrant]({{< relref "doc/packages/vagrant.en-us.md" >}}) | - | `vagrant` | **The following paragraphs only apply if Packages are not globally disabled!** diff --git a/docs/content/doc/packages/vagrant.en-us.md b/docs/content/doc/packages/vagrant.en-us.md new file mode 100644 index 0000000000000..e846de1a2a707 --- /dev/null +++ b/docs/content/doc/packages/vagrant.en-us.md @@ -0,0 +1,78 @@ +--- +date: "2022-08-23T00:00:00+00:00" +title: "Vagrant Packages Repository" +slug: "packages/vagrant" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "vagrant" + weight: 120 + identifier: "vagrant" +--- + +# Vagrant Packages Repository + +Publish [Vagrant](https://www.vagrantup.com/) packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Vagrant package registry, you need [Vagrant](https://www.vagrantup.com/downloads) and a tool to make HTTP requests like `curl`. + +## Publish a package + +Publish a Vagrant box by performing a HTTP PUT request to the registry: + +``` +PUT https://gitea.example.com/api/packages/{owner}/vagrant/{package_name}/{package_version}/{provider}.box +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the package. | +| `package_name` | The package name. | +| `package_version` | The package version, semver compatible. | +| `provider` | One of the [supported provider names](https://www.vagrantup.com/docs/providers). | + +Example for uploading a Hyper-V box: + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/your/vagrant.box \ + https://gitea.example.com/api/packages/testuser/vagrant/test_system/1.0.0/hyperv.box +``` + +You cannot publish a box if a box of the same name, version and provider already exists. You must delete the existing package first. + +## Install a package + +To install a box from the package registry, execute the following command: + +```shell +vagrant box add "https://gitea.example.com/api/packages/{owner}/vagrant/{package_name}" +``` + +| Parameter | Description | +| -------------- | ----------- | +| `owner` | The owner of the package. | +| `package_name` | The package name. | + +For example: + +```shell +vagrant box add "https://gitea.example.com/api/packages/testuser/vagrant/test_system" +``` + +This will install the latest version of the package. To add a specific version, use the `--box-version` parameter. +If the registry is private you can pass your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) in the `VAGRANT_CLOUD_TOKEN` environment variable. + +## Supported commands + +``` +vagrant box add +``` From 3b29fa649a5e1486c5b101a85a5b457e4152214c Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 23 Aug 2022 15:11:55 +0000 Subject: [PATCH 6/7] Add icons. --- public/img/svg/gitea-vagrant.svg | 1 + web_src/svg/gitea-vagrant.svg | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 public/img/svg/gitea-vagrant.svg create mode 100644 web_src/svg/gitea-vagrant.svg diff --git a/public/img/svg/gitea-vagrant.svg b/public/img/svg/gitea-vagrant.svg new file mode 100644 index 0000000000000..4c1b78cab5402 --- /dev/null +++ b/public/img/svg/gitea-vagrant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web_src/svg/gitea-vagrant.svg b/web_src/svg/gitea-vagrant.svg new file mode 100644 index 0000000000000..37bc1673cb4a8 --- /dev/null +++ b/web_src/svg/gitea-vagrant.svg @@ -0,0 +1,6 @@ + + + + + + From e84bf12189a684db301310f353cbad170eb527eb Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 26 Aug 2022 16:15:26 +0200 Subject: [PATCH 7/7] Update routers/api/packages/api.go --- routers/api/packages/api.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 6d096371e9303..1bb3b3218927b 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -268,7 +268,6 @@ func Routes() *web.Route { }) r.Group("/vagrant", func() { r.Group("/authenticate", func() { - // r.Post("", vagrant.Authenticate) r.Get("", vagrant.CheckAuthenticate) }) r.Group("/{name}", func() {