Skip to content

Commit 5968c63

Browse files
authored
Add Go package registry (#24687)
Fixes #7608 This PR adds a Go package registry usable with the Go proxy protocol. ![grafik](https://github.com/go-gitea/gitea/assets/1666336/328feb5c-3df2-4f9d-8eae-fe3126d14c37)
1 parent 53a0001 commit 5968c63

File tree

23 files changed

+751
-10
lines changed

23 files changed

+751
-10
lines changed

custom/conf/app.example.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,8 @@ ROUTER = console
24632463
;LIMIT_SIZE_DEBIAN = -1
24642464
;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
24652465
;LIMIT_SIZE_GENERIC = -1
2466+
;; Maximum size of a Go upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
2467+
;LIMIT_SIZE_GO = -1
24662468
;; Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
24672469
;LIMIT_SIZE_HELM = -1
24682470
;; Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)

docs/content/doc/administration/config-cheat-sheet.en-us.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
12231223
- `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
12241224
- `LIMIT_SIZE_DEBIAN`: **-1**: Maximum size of a Debian upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
12251225
- `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
1226+
- `LIMIT_SIZE_GO`: **-1**: Maximum size of a Go upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
12261227
- `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
12271228
- `LIMIT_SIZE_MAVEN`: **-1**: Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
12281229
- `LIMIT_SIZE_NPM`: **-1**: Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)

docs/content/doc/usage/packages/debian.en-us.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ curl --user your_username:your_password_or_token \
8383
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.
8484
You cannot publish a file with the same name twice to a package. You must delete the existing package version first.
8585

86-
The server reponds with the following HTTP Status codes.
86+
The server responds with the following HTTP Status codes.
8787

8888
| HTTP Status Code | Meaning |
8989
| ----------------- | ------- |
@@ -115,7 +115,7 @@ curl --user your_username:your_token_or_password -X DELETE \
115115
https://gitea.example.com/api/packages/testuser/debian/pools/bionic/main/test-package/1.0.0/amd64
116116
```
117117

118-
The server reponds with the following HTTP Status codes.
118+
The server responds with the following HTTP Status codes.
119119

120120
| HTTP Status Code | Meaning |
121121
| ----------------- | ------- |

docs/content/doc/usage/packages/generic.en-us.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ curl --user your_username:your_password_or_token \
5151

5252
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.
5353

54-
The server reponds with the following HTTP Status codes.
54+
The server responds with the following HTTP Status codes.
5555

5656
| HTTP Status Code | Meaning |
5757
| ----------------- | ------- |
@@ -83,7 +83,7 @@ curl --user your_username:your_token_or_password \
8383
https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0/file.bin
8484
```
8585

86-
The server reponds with the following HTTP Status codes.
86+
The server responds with the following HTTP Status codes.
8787

8888
| HTTP Status Code | Meaning |
8989
| ----------------- | ------- |
@@ -111,7 +111,7 @@ curl --user your_username:your_token_or_password -X DELETE \
111111
https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0
112112
```
113113

114-
The server reponds with the following HTTP Status codes.
114+
The server responds with the following HTTP Status codes.
115115

116116
| HTTP Status Code | Meaning |
117117
| ----------------- | ------- |
@@ -140,7 +140,7 @@ curl --user your_username:your_token_or_password -X DELETE \
140140
https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0/file.bin
141141
```
142142

143-
The server reponds with the following HTTP Status codes.
143+
The server responds with the following HTTP Status codes.
144144

145145
| HTTP Status Code | Meaning |
146146
| ----------------- | ------- |
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
date: "2023-05-10T00:00:00+00:00"
3+
title: "Go Packages Repository"
4+
slug: "go"
5+
weight: 45
6+
draft: false
7+
toc: false
8+
menu:
9+
sidebar:
10+
parent: "packages"
11+
name: "Go"
12+
weight: 45
13+
identifier: "go"
14+
---
15+
16+
# Go Packages Repository
17+
18+
Publish Go packages for your user or organization.
19+
20+
**Table of Contents**
21+
22+
{{< toc >}}
23+
24+
## Publish a package
25+
26+
To publish a Go package perform a HTTP `PUT` operation with the package content in the request body.
27+
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
28+
The package must follow the [documented structure](https://go.dev/ref/mod#zip-files).
29+
30+
```
31+
PUT https://gitea.example.com/api/packages/{owner}/go/upload
32+
```
33+
34+
| Parameter | Description |
35+
| --------- | ----------- |
36+
| `owner` | The owner of the package. |
37+
38+
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" >}}):
39+
40+
```shell
41+
curl --user your_username:your_password_or_token \
42+
--upload-file path/to/file.zip \
43+
https://gitea.example.com/api/packages/testuser/go/upload
44+
```
45+
46+
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.
47+
48+
The server responds with the following HTTP Status codes.
49+
50+
| HTTP Status Code | Meaning |
51+
| ----------------- | ------- |
52+
| `201 Created` | The package has been published. |
53+
| `400 Bad Request` | The package is invalid. |
54+
| `409 Conflict` | A package with the same name exist already. |
55+
56+
## Install a package
57+
58+
To install a Go package instruct Go to use the package registry as proxy:
59+
60+
```shell
61+
# use latest version
62+
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}
63+
# or
64+
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@latest
65+
# use specific version
66+
GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@{package_version}
67+
```
68+
69+
| Parameter | Description |
70+
| ----------------- | ----------- |
71+
| `owner` | The owner of the package. |
72+
| `package_name` | The package name. |
73+
| `package_version` | The package version. |
74+
75+
If the owner of the packages is private you need to [provide credentials](https://go.dev/ref/mod#private-module-proxy-auth).
76+
77+
More information about the `GOPROXY` environment variable and how to protect against data leaks can be found in [the documentation](https://go.dev/ref/mod#private-modules).

docs/content/doc/usage/packages/rpm.en-us.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ curl --user your_username:your_password_or_token \
6969
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.
7070
You cannot publish a file with the same name twice to a package. You must delete the existing package version first.
7171

72-
The server reponds with the following HTTP Status codes.
72+
The server responds with the following HTTP Status codes.
7373

7474
| HTTP Status Code | Meaning |
7575
| ----------------- | ------- |
@@ -99,7 +99,7 @@ curl --user your_username:your_token_or_password -X DELETE \
9999
https://gitea.example.com/api/packages/testuser/rpm/test-package/1.0.0/x86_64
100100
```
101101

102-
The server reponds with the following HTTP Status codes.
102+
The server responds with the following HTTP Status codes.
103103

104104
| HTTP Status Code | Meaning |
105105
| ----------------- | ------- |

models/packages/descriptor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
155155
metadata = &debian.Metadata{}
156156
case TypeGeneric:
157157
// generic packages have no metadata
158+
case TypeGo:
159+
// go packages have no metadata
158160
case TypeHelm:
159161
metadata = &helm.Metadata{}
160162
case TypeNuGet:

models/packages/package.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
TypeContainer Type = "container"
4040
TypeDebian Type = "debian"
4141
TypeGeneric Type = "generic"
42+
TypeGo Type = "go"
4243
TypeHelm Type = "helm"
4344
TypeMaven Type = "maven"
4445
TypeNpm Type = "npm"
@@ -61,6 +62,7 @@ var TypeList = []Type{
6162
TypeContainer,
6263
TypeDebian,
6364
TypeGeneric,
65+
TypeGo,
6466
TypeHelm,
6567
TypeMaven,
6668
TypeNpm,
@@ -94,6 +96,8 @@ func (pt Type) Name() string {
9496
return "Debian"
9597
case TypeGeneric:
9698
return "Generic"
99+
case TypeGo:
100+
return "Go"
97101
case TypeHelm:
98102
return "Helm"
99103
case TypeMaven:
@@ -139,6 +143,8 @@ func (pt Type) SVGName() string {
139143
return "gitea-debian"
140144
case TypeGeneric:
141145
return "octicon-package"
146+
case TypeGo:
147+
return "gitea-go"
142148
case TypeHelm:
143149
return "gitea-helm"
144150
case TypeMaven:

modules/packages/goproxy/metadata.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package goproxy
5+
6+
import (
7+
"archive/zip"
8+
"fmt"
9+
"io"
10+
"path"
11+
"strings"
12+
13+
"code.gitea.io/gitea/modules/util"
14+
)
15+
16+
const (
17+
PropertyGoMod = "go.mod"
18+
19+
maxGoModFileSize = 16 * 1024 * 1024 // https://go.dev/ref/mod#zip-path-size-constraints
20+
)
21+
22+
var (
23+
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure")
24+
ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large")
25+
)
26+
27+
type Package struct {
28+
Name string
29+
Version string
30+
GoMod string
31+
}
32+
33+
// ParsePackage parses the Go package file
34+
// https://go.dev/ref/mod#zip-files
35+
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
36+
archive, err := zip.NewReader(r, size)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
var p *Package
42+
43+
for _, file := range archive.File {
44+
nameAndVersion := path.Dir(file.Name)
45+
46+
parts := strings.SplitN(nameAndVersion, "@", 2)
47+
if len(parts) != 2 {
48+
continue
49+
}
50+
51+
versionParts := strings.SplitN(parts[1], "/", 2)
52+
53+
if p == nil {
54+
p = &Package{
55+
Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]),
56+
Version: versionParts[0],
57+
}
58+
}
59+
60+
if len(versionParts) > 1 {
61+
// files are expected in the "root" folder
62+
continue
63+
}
64+
65+
if path.Base(file.Name) == "go.mod" {
66+
if file.UncompressedSize64 > maxGoModFileSize {
67+
return nil, ErrGoModFileTooLarge
68+
}
69+
70+
f, err := archive.Open(file.Name)
71+
if err != nil {
72+
return nil, err
73+
}
74+
defer f.Close()
75+
76+
bytes, err := io.ReadAll(&io.LimitedReader{R: f, N: maxGoModFileSize})
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
p.GoMod = string(bytes)
82+
83+
return p, nil
84+
}
85+
}
86+
87+
if p == nil {
88+
return nil, ErrInvalidStructure
89+
}
90+
91+
p.GoMod = fmt.Sprintf("module %s", p.Name)
92+
93+
return p, nil
94+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package goproxy
5+
6+
import (
7+
"archive/zip"
8+
"bytes"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
const (
15+
packageName = "gitea.com/go-gitea/gitea"
16+
packageVersion = "v0.0.1"
17+
)
18+
19+
func TestParsePackage(t *testing.T) {
20+
createArchive := func(files map[string][]byte) *bytes.Reader {
21+
var buf bytes.Buffer
22+
zw := zip.NewWriter(&buf)
23+
for name, content := range files {
24+
w, _ := zw.Create(name)
25+
w.Write(content)
26+
}
27+
zw.Close()
28+
return bytes.NewReader(buf.Bytes())
29+
}
30+
31+
t.Run("EmptyPackage", func(t *testing.T) {
32+
data := createArchive(nil)
33+
34+
p, err := ParsePackage(data, int64(data.Len()))
35+
assert.Nil(t, p)
36+
assert.ErrorIs(t, err, ErrInvalidStructure)
37+
})
38+
39+
t.Run("InvalidNameOrVersionStructure", func(t *testing.T) {
40+
data := createArchive(map[string][]byte{
41+
packageName + "/" + packageVersion + "/go.mod": {},
42+
})
43+
44+
p, err := ParsePackage(data, int64(data.Len()))
45+
assert.Nil(t, p)
46+
assert.ErrorIs(t, err, ErrInvalidStructure)
47+
})
48+
49+
t.Run("GoModFileInWrongDirectory", func(t *testing.T) {
50+
data := createArchive(map[string][]byte{
51+
packageName + "@" + packageVersion + "/subdir/go.mod": {},
52+
})
53+
54+
p, err := ParsePackage(data, int64(data.Len()))
55+
assert.NotNil(t, p)
56+
assert.NoError(t, err)
57+
assert.Equal(t, packageName, p.Name)
58+
assert.Equal(t, packageVersion, p.Version)
59+
assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod)
60+
})
61+
62+
t.Run("Valid", func(t *testing.T) {
63+
data := createArchive(map[string][]byte{
64+
packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"),
65+
packageName + "@" + packageVersion + "/go.mod": []byte("valid"),
66+
})
67+
68+
p, err := ParsePackage(data, int64(data.Len()))
69+
assert.NotNil(t, p)
70+
assert.NoError(t, err)
71+
assert.Equal(t, packageName, p.Name)
72+
assert.Equal(t, packageVersion, p.Version)
73+
assert.Equal(t, "valid", p.GoMod)
74+
})
75+
}

modules/setting/packages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var (
3333
LimitSizeContainer int64
3434
LimitSizeDebian int64
3535
LimitSizeGeneric int64
36+
LimitSizeGo int64
3637
LimitSizeHelm int64
3738
LimitSizeMaven int64
3839
LimitSizeNpm int64
@@ -79,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
7980
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
8081
Packages.LimitSizeDebian = mustBytes(sec, "LIMIT_SIZE_DEBIAN")
8182
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
83+
Packages.LimitSizeGo = mustBytes(sec, "LIMIT_SIZE_GO")
8284
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
8385
Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN")
8486
Packages.LimitSizeNpm = mustBytes(sec, "LIMIT_SIZE_NPM")

0 commit comments

Comments
 (0)