Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Vagrant packages #20930

Merged
merged 14 commits into from
Aug 29, 2022
Merged
1 change: 1 addition & 0 deletions docs/content/doc/packages/overview.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!**

Expand Down
78 changes: 78 additions & 0 deletions docs/content/doc/packages/vagrant.en-us.md
Original file line number Diff line number Diff line change
@@ -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
```
170 changes: 170 additions & 0 deletions integrations/api_packages_vagrant_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
3 changes: 3 additions & 0 deletions models/packages/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)))
}
Expand Down
5 changes: 5 additions & 0 deletions models/packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
}
Expand Down Expand Up @@ -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)))
}
Expand Down
97 changes: 97 additions & 0 deletions modules/packages/vagrant/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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":
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
}
}

return m, nil
}
Loading