Skip to content

Commit 41c76ad

Browse files
KN4CK3Rlafriks6543
authored
Add support for Vagrant packages (#20930)
* Add support for Vagrant boxes. * Add authentication. * Add tests. * Add integration tests. * Add docs. * Add icons. * Update routers/api/packages/api.go Co-authored-by: Lauris BH <[email protected]> Co-authored-by: 6543 <[email protected]>
1 parent 8a66b01 commit 41c76ad

File tree

19 files changed

+757
-2
lines changed

19 files changed

+757
-2
lines changed

docs/content/doc/packages/overview.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The following package managers are currently supported:
3737
| [Pub]({{< relref "doc/packages/pub.en-us.md" >}}) | Dart | `dart`, `flutter` |
3838
| [PyPI]({{< relref "doc/packages/pypi.en-us.md" >}}) | Python | `pip`, `twine` |
3939
| [RubyGems]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Ruby | `gem`, `Bundler` |
40+
| [Vagrant]({{< relref "doc/packages/vagrant.en-us.md" >}}) | - | `vagrant` |
4041

4142
**The following paragraphs only apply if Packages are not globally disabled!**
4243

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
date: "2022-08-23T00:00:00+00:00"
3+
title: "Vagrant Packages Repository"
4+
slug: "packages/vagrant"
5+
draft: false
6+
toc: false
7+
menu:
8+
sidebar:
9+
parent: "packages"
10+
name: "vagrant"
11+
weight: 120
12+
identifier: "vagrant"
13+
---
14+
15+
# Vagrant Packages Repository
16+
17+
Publish [Vagrant](https://www.vagrantup.com/) packages for your user or organization.
18+
19+
**Table of Contents**
20+
21+
{{< toc >}}
22+
23+
## Requirements
24+
25+
To work with the Vagrant package registry, you need [Vagrant](https://www.vagrantup.com/downloads) and a tool to make HTTP requests like `curl`.
26+
27+
## Publish a package
28+
29+
Publish a Vagrant box by performing a HTTP PUT request to the registry:
30+
31+
```
32+
PUT https://gitea.example.com/api/packages/{owner}/vagrant/{package_name}/{package_version}/{provider}.box
33+
```
34+
35+
| Parameter | Description |
36+
| ----------------- | ----------- |
37+
| `owner` | The owner of the package. |
38+
| `package_name` | The package name. |
39+
| `package_version` | The package version, semver compatible. |
40+
| `provider` | One of the [supported provider names](https://www.vagrantup.com/docs/providers). |
41+
42+
Example for uploading a Hyper-V box:
43+
44+
```shell
45+
curl --user your_username:your_password_or_token \
46+
--upload-file path/to/your/vagrant.box \
47+
https://gitea.example.com/api/packages/testuser/vagrant/test_system/1.0.0/hyperv.box
48+
```
49+
50+
You cannot publish a box if a box of the same name, version and provider already exists. You must delete the existing package first.
51+
52+
## Install a package
53+
54+
To install a box from the package registry, execute the following command:
55+
56+
```shell
57+
vagrant box add "https://gitea.example.com/api/packages/{owner}/vagrant/{package_name}"
58+
```
59+
60+
| Parameter | Description |
61+
| -------------- | ----------- |
62+
| `owner` | The owner of the package. |
63+
| `package_name` | The package name. |
64+
65+
For example:
66+
67+
```shell
68+
vagrant box add "https://gitea.example.com/api/packages/testuser/vagrant/test_system"
69+
```
70+
71+
This will install the latest version of the package. To add a specific version, use the `--box-version` parameter.
72+
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.
73+
74+
## Supported commands
75+
76+
```
77+
vagrant box add
78+
```
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"archive/tar"
9+
"bytes"
10+
"compress/gzip"
11+
"fmt"
12+
"net/http"
13+
"strings"
14+
"testing"
15+
16+
"code.gitea.io/gitea/models/db"
17+
"code.gitea.io/gitea/models/packages"
18+
"code.gitea.io/gitea/models/unittest"
19+
user_model "code.gitea.io/gitea/models/user"
20+
"code.gitea.io/gitea/modules/json"
21+
vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestPackageVagrant(t *testing.T) {
27+
defer prepareTestEnv(t)()
28+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
29+
30+
token := "Bearer " + getUserToken(t, user.Name)
31+
32+
packageName := "test_package"
33+
packageVersion := "1.0.1"
34+
packageDescription := "Test Description"
35+
packageProvider := "virtualbox"
36+
37+
filename := fmt.Sprintf("%s.box", packageProvider)
38+
39+
infoContent, _ := json.Marshal(map[string]string{
40+
"description": packageDescription,
41+
})
42+
43+
var buf bytes.Buffer
44+
zw := gzip.NewWriter(&buf)
45+
archive := tar.NewWriter(zw)
46+
archive.WriteHeader(&tar.Header{
47+
Name: "info.json",
48+
Mode: 0o600,
49+
Size: int64(len(infoContent)),
50+
})
51+
archive.Write(infoContent)
52+
archive.Close()
53+
zw.Close()
54+
content := buf.Bytes()
55+
56+
root := fmt.Sprintf("/api/packages/%s/vagrant", user.Name)
57+
58+
t.Run("Authenticate", func(t *testing.T) {
59+
defer PrintCurrentTest(t)()
60+
61+
authenticateURL := fmt.Sprintf("%s/authenticate", root)
62+
63+
req := NewRequest(t, "GET", authenticateURL)
64+
MakeRequest(t, req, http.StatusUnauthorized)
65+
66+
req = NewRequest(t, "GET", authenticateURL)
67+
addTokenAuthHeader(req, token)
68+
MakeRequest(t, req, http.StatusOK)
69+
})
70+
71+
boxURL := fmt.Sprintf("%s/%s", root, packageName)
72+
73+
t.Run("Upload", func(t *testing.T) {
74+
defer PrintCurrentTest(t)()
75+
76+
req := NewRequest(t, "HEAD", boxURL)
77+
MakeRequest(t, req, http.StatusNotFound)
78+
79+
uploadURL := fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename)
80+
81+
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
82+
MakeRequest(t, req, http.StatusUnauthorized)
83+
84+
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
85+
addTokenAuthHeader(req, token)
86+
MakeRequest(t, req, http.StatusCreated)
87+
88+
req = NewRequest(t, "HEAD", boxURL)
89+
resp := MakeRequest(t, req, http.StatusOK)
90+
assert.True(t, strings.HasPrefix(resp.HeaderMap.Get("Content-Type"), "application/json"))
91+
92+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeVagrant)
93+
assert.NoError(t, err)
94+
assert.Len(t, pvs, 1)
95+
96+
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
97+
assert.NoError(t, err)
98+
assert.NotNil(t, pd.SemVer)
99+
assert.IsType(t, &vagrant_module.Metadata{}, pd.Metadata)
100+
assert.Equal(t, packageName, pd.Package.Name)
101+
assert.Equal(t, packageVersion, pd.Version.Version)
102+
103+
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
104+
assert.NoError(t, err)
105+
assert.Len(t, pfs, 1)
106+
assert.Equal(t, filename, pfs[0].Name)
107+
assert.True(t, pfs[0].IsLead)
108+
109+
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
110+
assert.NoError(t, err)
111+
assert.Equal(t, int64(len(content)), pb.Size)
112+
113+
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
114+
addTokenAuthHeader(req, token)
115+
MakeRequest(t, req, http.StatusConflict)
116+
})
117+
118+
t.Run("Download", func(t *testing.T) {
119+
defer PrintCurrentTest(t)()
120+
121+
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename))
122+
resp := MakeRequest(t, req, http.StatusOK)
123+
124+
assert.Equal(t, content, resp.Body.Bytes())
125+
})
126+
127+
t.Run("EnumeratePackageVersions", func(t *testing.T) {
128+
defer PrintCurrentTest(t)()
129+
130+
req := NewRequest(t, "GET", boxURL)
131+
resp := MakeRequest(t, req, http.StatusOK)
132+
133+
type providerData struct {
134+
Name string `json:"name"`
135+
URL string `json:"url"`
136+
Checksum string `json:"checksum"`
137+
ChecksumType string `json:"checksum_type"`
138+
}
139+
140+
type versionMetadata struct {
141+
Version string `json:"version"`
142+
Status string `json:"status"`
143+
DescriptionHTML string `json:"description_html,omitempty"`
144+
DescriptionMarkdown string `json:"description_markdown,omitempty"`
145+
Providers []*providerData `json:"providers"`
146+
}
147+
148+
type packageMetadata struct {
149+
Name string `json:"name"`
150+
Description string `json:"description,omitempty"`
151+
ShortDescription string `json:"short_description,omitempty"`
152+
Versions []*versionMetadata `json:"versions"`
153+
}
154+
155+
var result packageMetadata
156+
DecodeJSON(t, resp, &result)
157+
158+
assert.Equal(t, packageName, result.Name)
159+
assert.Equal(t, packageDescription, result.Description)
160+
assert.Len(t, result.Versions, 1)
161+
version := result.Versions[0]
162+
assert.Equal(t, packageVersion, version.Version)
163+
assert.Equal(t, "active", version.Status)
164+
assert.Len(t, version.Providers, 1)
165+
provider := version.Providers[0]
166+
assert.Equal(t, packageProvider, provider.Name)
167+
assert.Equal(t, "sha512", provider.ChecksumType)
168+
assert.Equal(t, "259bebd6160acad695016d22a45812e26f187aaf78e71a4c23ee3201528346293f991af3468a8c6c5d2a21d7d9e1bdc1bf79b87110b2fddfcc5a0d45963c7c30", provider.Checksum)
169+
})
170+
}

models/packages/descriptor.go

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/modules/packages/pub"
2323
"code.gitea.io/gitea/modules/packages/pypi"
2424
"code.gitea.io/gitea/modules/packages/rubygems"
25+
"code.gitea.io/gitea/modules/packages/vagrant"
2526

2627
"github.com/hashicorp/go-version"
2728
)
@@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
150151
metadata = &pypi.Metadata{}
151152
case TypeRubyGems:
152153
metadata = &rubygems.Metadata{}
154+
case TypeVagrant:
155+
metadata = &vagrant.Metadata{}
153156
default:
154157
panic(fmt.Sprintf("unknown package type: %s", string(p.Type)))
155158
}

models/packages/package.go

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const (
4242
TypePub Type = "pub"
4343
TypePyPI Type = "pypi"
4444
TypeRubyGems Type = "rubygems"
45+
TypeVagrant Type = "vagrant"
4546
)
4647

4748
// Name gets the name of the package type
@@ -69,6 +70,8 @@ func (pt Type) Name() string {
6970
return "PyPI"
7071
case TypeRubyGems:
7172
return "RubyGems"
73+
case TypeVagrant:
74+
return "Vagrant"
7275
}
7376
panic(fmt.Sprintf("unknown package type: %s", string(pt)))
7477
}
@@ -98,6 +101,8 @@ func (pt Type) SVGName() string {
98101
return "gitea-python"
99102
case TypeRubyGems:
100103
return "gitea-rubygems"
104+
case TypeVagrant:
105+
return "gitea-vagrant"
101106
}
102107
panic(fmt.Sprintf("unknown package type: %s", string(pt)))
103108
}

modules/packages/vagrant/metadata.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package vagrant
6+
7+
import (
8+
"archive/tar"
9+
"compress/gzip"
10+
"io"
11+
"strings"
12+
13+
"code.gitea.io/gitea/modules/json"
14+
"code.gitea.io/gitea/modules/validation"
15+
)
16+
17+
const (
18+
PropertyProvider = "vagrant.provider"
19+
)
20+
21+
// Metadata represents the metadata of a Vagrant package
22+
type Metadata struct {
23+
Author string `json:"author,omitempty"`
24+
Description string `json:"description,omitempty"`
25+
ProjectURL string `json:"project_url,omitempty"`
26+
RepositoryURL string `json:"repository_url,omitempty"`
27+
}
28+
29+
// ParseMetadataFromBox parses the metdata of a box file
30+
func ParseMetadataFromBox(r io.Reader) (*Metadata, error) {
31+
gzr, err := gzip.NewReader(r)
32+
if err != nil {
33+
return nil, err
34+
}
35+
defer gzr.Close()
36+
37+
tr := tar.NewReader(gzr)
38+
for {
39+
hd, err := tr.Next()
40+
if err == io.EOF {
41+
break
42+
}
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
if hd.Typeflag != tar.TypeReg {
48+
continue
49+
}
50+
51+
if hd.Name == "info.json" {
52+
return ParseInfoFile(tr)
53+
}
54+
}
55+
56+
return &Metadata{}, nil
57+
}
58+
59+
// ParseInfoFile parses a info.json file to retrieve the metadata of a Vagrant package
60+
func ParseInfoFile(r io.Reader) (*Metadata, error) {
61+
var values map[string]string
62+
if err := json.NewDecoder(r).Decode(&values); err != nil {
63+
return nil, err
64+
}
65+
66+
m := &Metadata{}
67+
68+
// There is no defined format for this file, just try the common keys
69+
for k, v := range values {
70+
switch strings.ToLower(k) {
71+
case "description":
72+
fallthrough
73+
case "short_description":
74+
m.Description = v
75+
case "website":
76+
fallthrough
77+
case "homepage":
78+
fallthrough
79+
case "url":
80+
if validation.IsValidURL(v) {
81+
m.ProjectURL = v
82+
}
83+
case "repository":
84+
fallthrough
85+
case "source":
86+
if validation.IsValidURL(v) {
87+
m.RepositoryURL = v
88+
}
89+
case "author":
90+
fallthrough
91+
case "authors":
92+
m.Author = v
93+
}
94+
}
95+
96+
return m, nil
97+
}

0 commit comments

Comments
 (0)