Skip to content

Fix some package registry problems #34759

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

Merged
merged 1 commit into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions models/packages/package_property.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error {
return err
}

// DeletePropertyByName deletes properties by name
func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error {
// DeletePropertiesByName deletes properties by name
func DeletePropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) error {
_, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{})
return err
}
Expand Down
3 changes: 1 addition & 2 deletions modules/packages/content_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ func NewContentStore() *ContentStore {
return contentStore
}

// Get gets a package blob
func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) {
func (s *ContentStore) OpenBlob(key BlobHash256Key) (storage.Object, error) {
return s.store.Open(KeyToRelativePath(key))
}

Expand Down
42 changes: 9 additions & 33 deletions routers/api/packages/container/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"

"github.com/opencontainers/go-digest"
oci "github.com/opencontainers/image-spec/specs-go/v1"
Expand Down Expand Up @@ -84,12 +85,11 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf
manifestDigest := ""

err := func() error {
var manifest oci.Manifest
if err := json.NewDecoder(buf).Decode(&manifest); err != nil {
manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image)
if err != nil {
return err
}

if _, err := buf.Seek(0, io.SeekStart); err != nil {
if _, err = buf.Seek(0, io.SeekStart); err != nil {
return err
}

Expand All @@ -99,28 +99,7 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf
}
defer committer.Close()

configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
OwnerID: mci.Owner.ID,
Image: mci.Image,
Digest: string(manifest.Config.Digest),
})
if err != nil {
return err
}

configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256))
if err != nil {
return err
}
defer configReader.Close()

metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
if err != nil {
return err
}

blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))

blobReferences = append(blobReferences, &blobReference{
Digest: manifest.Config.Digest,
MediaType: manifest.Config.MediaType,
Expand Down Expand Up @@ -388,19 +367,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
return nil, err
}
} else {
props, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged)
if err != nil {
if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil {
return nil, err
}
for _, prop := range props {
if err = packages_model.DeletePropertyByID(ctx, prop.ID); err != nil {
return nil, err
}
}
}

if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil {
return nil, err
}
for _, manifest := range metadata.Manifests {
if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
return nil, err
}
}
Expand Down
51 changes: 41 additions & 10 deletions routers/web/user/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package user

import (
gocontext "context"
"errors"
"net/http"
"net/url"

Expand All @@ -20,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/optional"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
arch_module "code.gitea.io/gitea/modules/packages/arch"
container_module "code.gitea.io/gitea/modules/packages/container"
debian_module "code.gitea.io/gitea/modules/packages/debian"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
Expand All @@ -31,6 +34,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
)

const (
Expand Down Expand Up @@ -162,13 +166,32 @@ func RedirectToLastVersion(ctx *context.Context) {
ctx.Redirect(pd.VersionWebLink())
}

func viewPackageContainerImage(ctx gocontext.Context, pd *packages_model.PackageDescriptor, digest string) (*container_module.Metadata, error) {
manifestBlob, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
OwnerID: pd.Owner.ID,
Image: pd.Package.LowerName,
Digest: digest,
})
if err != nil {
return nil, err
}
manifestReader, err := packages_service.OpenBlobStream(manifestBlob.Blob)
if err != nil {
return nil, err
}
defer manifestReader.Close()
_, _, metadata, err := container_service.ParseManifestMetadata(ctx, manifestReader, pd.Owner.ID, pd.Package.LowerName)
return metadata, err
}

// ViewPackageVersion displays a single package version
func ViewPackageVersion(ctx *context.Context) {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return
}

versionSub := ctx.PathParam("version_sub")
pd := ctx.Package.Descriptor
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
Expand All @@ -180,6 +203,9 @@ func ViewPackageVersion(ctx *context.Context) {
}
ctx.Data["PackageRegistryHost"] = registryHostURL.Host

var pvs []*packages_model.PackageVersion
pvsTotal := int64(0)

switch pd.Package.Type {
case packages_model.TypeAlpine:
branches := make(container.Set[string])
Expand Down Expand Up @@ -257,21 +283,26 @@ func ViewPackageVersion(ctx *context.Context) {

ctx.Data["Groups"] = util.Sorted(groups.Values())
ctx.Data["Architectures"] = util.Sorted(architectures.Values())
}

var (
total int64
pvs []*packages_model.PackageVersion
)
switch pd.Package.Type {
case packages_model.TypeContainer:
pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
imageMetadata := pd.Metadata
if versionSub != "" {
imageMetadata, err = viewPackageContainerImage(ctx, pd, versionSub)
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound(nil)
return
} else if err != nil {
ctx.ServerError("viewPackageContainerImage", err)
return
}
}
ctx.Data["ContainerImageMetadata"] = imageMetadata
pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsTagged: true,
})
default:
pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsInternal: optional.Some(false),
Expand All @@ -283,7 +314,7 @@ func ViewPackageVersion(ctx *context.Context) {
}

ctx.Data["LatestVersions"] = pvs
ctx.Data["TotalVersionCount"] = total
ctx.Data["TotalVersionCount"] = pvsTotal

ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()

Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/versions", user.ListPackageVersions)
m.Group("/{version}", func() {
m.Get("", user.ViewPackageVersion)
m.Get("/{version_sub}", user.ViewPackageVersion)
m.Get("/files/{fileid}", user.DownloadPackageFile)
m.Group("/settings", func() {
m.Get("", user.PackageSettings)
Expand Down
31 changes: 30 additions & 1 deletion services/packages/container/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ package container

import (
"context"
"io"
"strings"

packages_model "code.gitea.io/gitea/models/packages"
container_service "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"

"github.com/opencontainers/image-spec/specs-go/v1"
)

// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
Expand All @@ -22,7 +28,7 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner
newOwnerName = strings.ToLower(newOwnerName)

for _, p := range ps {
if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
if err := packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
return err
}

Expand All @@ -33,3 +39,26 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner

return nil
}

func ParseManifestMetadata(ctx context.Context, rd io.Reader, ownerID int64, imageName string) (*v1.Manifest, *packages_model.PackageFileDescriptor, *container_module.Metadata, error) {
var manifest v1.Manifest
if err := json.NewDecoder(rd).Decode(&manifest); err != nil {
return nil, nil, nil, err
}
configDescriptor, err := container_service.GetContainerBlob(ctx, &container_service.BlobSearchOptions{
OwnerID: ownerID,
Image: imageName,
Digest: string(manifest.Config.Digest),
})
if err != nil {
return nil, nil, nil, err
}

configReader, err := packages.NewContentStore().OpenBlob(packages.BlobHash256Key(configDescriptor.Blob.HashSHA256))
if err != nil {
return nil, nil, nil, err
}
defer configReader.Close()
metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
return &manifest, configDescriptor, metadata, err
}
8 changes: 7 additions & 1 deletion services/packages/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,12 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (
return GetPackageBlobStream(ctx, pf, pb, nil)
}

func OpenBlobStream(pb *packages_model.PackageBlob) (io.ReadSeekCloser, error) {
cs := packages_module.NewContentStore()
key := packages_module.BlobHash256Key(pb.HashSHA256)
return cs.OpenBlob(key)
}

// GetPackageBlobStream returns the content of the specific package blob
// If the storage supports direct serving and it's enabled, only the direct serving url is returned.
func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
Expand All @@ -617,7 +623,7 @@ func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, p
}
}
if u == nil {
s, err = cs.Get(key)
s, err = cs.OpenBlob(key)
}

if err == nil {
Expand Down
28 changes: 22 additions & 6 deletions templates/package/content/container.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@
{{/* "unknown/unknown" is attestation-manifest, so we should skip it */}}
{{if ne .Platform "unknown/unknown"}}
<tr>
<td><a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}</a></td>
<td>
<a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{$.PackageDescriptor.Version.LowerVersion}}/{{PathEscape .Digest}}">
{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}
</a>
</td>
<td>{{.Platform}}</td>
<td>{{FileSize .Size}}</td>
</tr>
Expand All @@ -65,12 +69,24 @@
{{.PackageDescriptor.Metadata.Description}}
</div>
{{end}}
{{if .PackageDescriptor.Metadata.ImageLayers}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.layers"}}</h4>

{{/* a container manifest may contain sub manifests, so here we try to display some information of the sub manifest,
not perfect, just better than before */}}
{{$imageMetadata := .ContainerImageMetadata}}
{{if $imageMetadata.ImageLayers}}
<h4 class="ui top attached header flex-text-block">
{{ctx.Locale.Tr "packages.container.layers"}}
{{/* only show the platform if the image metadata is not the package's, which means that it is a sub manifest */}}
{{if ne .ContainerImageMetadata .PackageDescriptor.Metadata}}
<span class="tw-text-sm flex-text-inline" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">
({{svg "octicon-cpu" 12}} {{.ContainerImageMetadata.Platform}})
</span>
{{end}}
</h4>
<div class="ui attached segment tw-break-anywhere">
<table class="ui very basic compact table">
<tbody>
{{range .PackageDescriptor.Metadata.ImageLayers}}
{{range $imageMetadata.ImageLayers}}
<tr>
<td>{{.}}</td>
</tr>
Expand All @@ -79,7 +95,7 @@
</table>
</div>
{{end}}
{{if .PackageDescriptor.Metadata.Labels}}
{{if $imageMetadata.Labels}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.labels"}}</h4>
<div class="ui attached segment">
<table class="ui very basic compact table">
Expand All @@ -90,7 +106,7 @@
</tr>
</thead>
<tbody>
{{range $key, $value := .PackageDescriptor.Metadata.Labels}}
{{range $key, $value := $imageMetadata.Labels}}
<tr>
<td class="tw-align-top">{{$key}}</td>
<td class="tw-break-anywhere">{{$value}}</td>
Expand Down
2 changes: 1 addition & 1 deletion templates/package/content/pypi.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="ui form">
<div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label>
<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div>
<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> --extra-index-url https://pypi.org/ {{.PackageDescriptor.Package.Name}}</code></pre></div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label>
Expand Down
5 changes: 3 additions & 2 deletions templates/package/shared/view.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<div class="issue-title-header">
{{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}}
<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
<div>
{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
Expand Down Expand Up @@ -74,7 +75,7 @@
<div class="ui relaxed list">
{{range .PackageDescriptor.Files}}
<div class="item">
<a href="{{$.Link}}/files/{{.File.ID}}">{{.File.Name}}</a>
<a href="{{$packageVersionLink}}/files/{{.File.ID}}">{{.File.Name}}</a>
<span class="text small file-size">{{FileSize .Blob.Size}}</span>
</div>
{{end}}
Expand All @@ -98,7 +99,7 @@
<div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
{{end}}
{{if .CanWritePackages}}
<div class="item">{{svg "octicon-tools"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
<div class="item">{{svg "octicon-tools"}} <a href="{{$packageVersionLink}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
{{end}}
</div>
{{end}}
Expand Down
3 changes: 1 addition & 2 deletions tests/integration/api_packages_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,7 @@ func TestPackageContainer(t *testing.T) {
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))

// only the last manifest digest is associated with the version (OCI builders will push the index manifest digest as the final step)
assert.ElementsMatch(t, []string{untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference))
assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference))

assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
metadata := pd.Metadata.(*container_module.Metadata)
Expand Down