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 Go package registry #24687

Merged
merged 15 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,8 @@ ROUTER = console
;LIMIT_SIZE_DEBIAN = -1
;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_GENERIC = -1
;; Maximum size of a Go upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_GO = -1
;; Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_HELM = -1
;; Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
- `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_DEBIAN`: **-1**: Maximum size of a Debian upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_GO`: **-1**: Maximum size of a Go upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_MAVEN`: **-1**: Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_NPM`: **-1**: Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
Expand Down
76 changes: 76 additions & 0 deletions docs/content/doc/usage/packages/go.en-us.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
date: "2023-05-10T00:00:00+00:00"
title: "Go Packages Repository"
slug: "go"
weight: 45
draft: false
toc: false
menu:
sidebar:
parent: "packages"
name: "Go"
weight: 45
identifier: "go"
---

# Go Packages Repository

Publish Go packages for your user or organization.

**Table of Contents**

{{< toc >}}

## Publish a package

To publish a Go package perform a HTTP `PUT` operation with the package content in the request body.
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
The package must follow the [documented structure](https://go.dev/ref/mod#zip-files).

```
PUT https://gitea.example.com/api/packages/{owner}/go/upload
```

| Parameter | Description |
| --------- | ----------- |
| `owner` | The owner of the package. |

To authenticate to the package registry, you need to provide [custom HTTP headers or use HTTP Basic authentication]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}):

```shell
curl --user your_username:your_password_or_token \
--upload-file path/to/file.zip \
https://gitea.example.com/api/packages/testuser/go/upload
```

If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password.

The server reponds with the following HTTP Status codes.
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved

| HTTP Status Code | Meaning |
| ----------------- | ------- |
| `201 Created` | The package has been published. |
| `400 Bad Request` | The package is invalid. |
| `409 Conflict` | A package with the same name exist already. |

## Install a package

To install a Go package instruct Go to use the package registry as proxy:

```shell
# use latest version
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}
# or
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@latest
# use specific version
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@{package_version}
```

| Parameter | Description |
| ----------------- | ----------- |
| `owner` | The owner of the package. |
| `package_name` | The package name. |
| `package_version` | The package version. |

If the owner of the packages is private you need to provide credentials.
For more information see [the Go documentation](https://go.dev/ref/mod#private-module-proxy-auth).
2 changes: 2 additions & 0 deletions models/packages/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
metadata = &debian.Metadata{}
case TypeGeneric:
// generic packages have no metadata
case TypeGo:
// go packages have no metadata
case TypeHelm:
metadata = &helm.Metadata{}
case TypeNuGet:
Expand Down
6 changes: 6 additions & 0 deletions models/packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
TypeContainer Type = "container"
TypeDebian Type = "debian"
TypeGeneric Type = "generic"
TypeGo Type = "go"
TypeHelm Type = "helm"
TypeMaven Type = "maven"
TypeNpm Type = "npm"
Expand All @@ -61,6 +62,7 @@ var TypeList = []Type{
TypeContainer,
TypeDebian,
TypeGeneric,
TypeGo,
TypeHelm,
TypeMaven,
TypeNpm,
Expand Down Expand Up @@ -94,6 +96,8 @@ func (pt Type) Name() string {
return "Debian"
case TypeGeneric:
return "Generic"
case TypeGo:
return "Go"
case TypeHelm:
return "Helm"
case TypeMaven:
Expand Down Expand Up @@ -139,6 +143,8 @@ func (pt Type) SVGName() string {
return "gitea-debian"
case TypeGeneric:
return "octicon-package"
case TypeGo:
return "gitea-go"
case TypeHelm:
return "gitea-helm"
case TypeMaven:
Expand Down
94 changes: 94 additions & 0 deletions modules/packages/goproxy/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package goproxy

import (
"archive/zip"
"fmt"
"io"
"path"
"strings"

"code.gitea.io/gitea/modules/util"
)

const (
PropertyGoMod = "go.mod"

maxGoModFileSize = 16 * 1024 * 1024 // https://go.dev/ref/mod#zip-path-size-constraints
)

var (
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure")
ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large")
)

type Package struct {
Name string
Version string
GoMod string
}

// ParsePackage parses the Go package file
// https://go.dev/ref/mod#zip-files
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
archive, err := zip.NewReader(r, size)
if err != nil {
return nil, err
}

var p *Package

for _, file := range archive.File {
nameAndVersion := path.Dir(file.Name)

parts := strings.SplitN(nameAndVersion, "@", 2)
if len(parts) != 2 {
continue
}

versionParts := strings.SplitN(parts[1], "/", 2)

if p == nil {
p = &Package{
Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]),
Version: versionParts[0],
}
}

if len(versionParts) > 1 {
// files are expected in the "root" folder
continue
}

if path.Base(file.Name) == "go.mod" {
if file.UncompressedSize64 > maxGoModFileSize {
return nil, ErrGoModFileTooLarge
}

f, err := archive.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()

bytes, err := io.ReadAll(&io.LimitedReader{R: f, N: maxGoModFileSize})
if err != nil {
return nil, err
}

p.GoMod = string(bytes)

return p, nil
}
}

if p == nil {
return nil, ErrInvalidStructure
}

p.GoMod = fmt.Sprintf("module %s", p.Name)

return p, nil
}
75 changes: 75 additions & 0 deletions modules/packages/goproxy/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package goproxy

import (
"archive/zip"
"bytes"
"testing"

"github.com/stretchr/testify/assert"
)

const (
packageName = "gitea.com/go-gitea/gitea"
packageVersion = "v0.0.1"
)

func TestParsePackage(t *testing.T) {
createArchive := func(files map[string][]byte) *bytes.Reader {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for name, content := range files {
w, _ := zw.Create(name)
w.Write(content)
}
zw.Close()
return bytes.NewReader(buf.Bytes())
}

t.Run("EmptyPackage", func(t *testing.T) {
data := createArchive(nil)

p, err := ParsePackage(data, int64(data.Len()))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidStructure)
})

t.Run("InvalidNameOrVersionStructure", func(t *testing.T) {
data := createArchive(map[string][]byte{
packageName + "/" + packageVersion + "/go.mod": {},
})

p, err := ParsePackage(data, int64(data.Len()))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidStructure)
})

t.Run("GoModFileInWrongDirectory", func(t *testing.T) {
data := createArchive(map[string][]byte{
packageName + "@" + packageVersion + "/subdir/go.mod": {},
})

p, err := ParsePackage(data, int64(data.Len()))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod)
})

t.Run("Valid", func(t *testing.T) {
data := createArchive(map[string][]byte{
packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"),
packageName + "@" + packageVersion + "/go.mod": []byte("valid"),
})

p, err := ParsePackage(data, int64(data.Len()))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, "valid", p.GoMod)
})
}
2 changes: 2 additions & 0 deletions modules/setting/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
LimitSizeContainer int64
LimitSizeDebian int64
LimitSizeGeneric int64
LimitSizeGo int64
LimitSizeHelm int64
LimitSizeMaven int64
LimitSizeNpm int64
Expand Down Expand Up @@ -79,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
Packages.LimitSizeDebian = mustBytes(sec, "LIMIT_SIZE_DEBIAN")
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
Packages.LimitSizeGo = mustBytes(sec, "LIMIT_SIZE_GO")
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN")
Packages.LimitSizeNpm = mustBytes(sec, "LIMIT_SIZE_NPM")
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3263,6 +3263,8 @@ debian.repository.components = Components
debian.repository.architectures = Architectures
generic.download = Download package from the command line:
generic.documentation = For more information on the generic registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
go.install = Install the package from the command line:
go.documentation = For more information on the Go registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
helm.registry = Setup this registry from the command line:
helm.install = To install the package, run the following command:
helm.documentation = For more information on the Helm registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
Expand Down
1 change: 1 addition & 0 deletions public/img/svg/gitea-go.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/container"
"code.gitea.io/gitea/routers/api/packages/debian"
"code.gitea.io/gitea/routers/api/packages/generic"
"code.gitea.io/gitea/routers/api/packages/goproxy"
"code.gitea.io/gitea/routers/api/packages/helm"
"code.gitea.io/gitea/routers/api/packages/maven"
"code.gitea.io/gitea/routers/api/packages/npm"
Expand Down Expand Up @@ -312,6 +313,55 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
}, reqPackageAccess(perm.AccessModeWrite))
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/go", func() {
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
ctx.Status(http.StatusNotFound)
})

// Manual mapping of routes because the package name contains slashes which chi does not support
r.Get("/*", func(ctx *context.Context) {
path := ctx.Params("*")

parts := strings.SplitN(path, "/@v/", 2)
if len(parts) != 2 {
ctx.Status(http.StatusNotFound)
return
}

ctx.SetParams("name", parts[0])

// <package/name>/@v/list
if parts[1] == "list" {
goproxy.EnumeratePackageVersions(ctx)
return
}

// <package/name>/@v/<version>.zip
if strings.HasSuffix(parts[1], ".zip") {
ctx.SetParams("version", parts[1][:len(parts[1])-len(".zip")])

goproxy.DownloadPackageFile(ctx)
return
}
// <package/name>/@v/<version>.info
if strings.HasSuffix(parts[1], ".info") {
ctx.SetParams("version", parts[1][:len(parts[1])-len(".info")])

goproxy.PackageVersionMetadata(ctx)
return
}
// <package/name>/@v/<version>.mod
if strings.HasSuffix(parts[1], ".mod") {
ctx.SetParams("version", parts[1][:len(parts[1])-len(".mod")])

goproxy.PackageVersionGoModContent(ctx)
return
}

ctx.Status(http.StatusNotFound)
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() {
r.Group("/{packagename}/{packageversion}", func() {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
Expand Down
Loading