Fix some package registry problems (#34759)

1. Fix #33787
2. Fix container image display
This commit is contained in:
wxiaoguang 2025-06-19 00:32:43 +08:00 committed by GitHub
parent 7954f25290
commit a2ae7c69da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 118 additions and 60 deletions

View File

@ -92,8 +92,8 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error {
return err return err
} }
// DeletePropertyByName deletes properties by name // DeletePropertiesByName deletes properties by name
func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error { 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{}) _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{})
return err return err
} }

View File

@ -28,8 +28,7 @@ func NewContentStore() *ContentStore {
return contentStore return contentStore
} }
// Get gets a package blob func (s *ContentStore) OpenBlob(key BlobHash256Key) (storage.Object, error) {
func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) {
return s.store.Open(KeyToRelativePath(key)) return s.store.Open(KeyToRelativePath(key))
} }

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
oci "github.com/opencontainers/image-spec/specs-go/v1" oci "github.com/opencontainers/image-spec/specs-go/v1"
@ -84,12 +85,11 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf
manifestDigest := "" manifestDigest := ""
err := func() error { err := func() error {
var manifest oci.Manifest manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image)
if err := json.NewDecoder(buf).Decode(&manifest); err != nil { if err != nil {
return err return err
} }
if _, err = buf.Seek(0, io.SeekStart); err != nil {
if _, err := buf.Seek(0, io.SeekStart); err != nil {
return err return err
} }
@ -99,28 +99,7 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf
} }
defer committer.Close() 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 := make([]*blobReference, 0, 1+len(manifest.Layers))
blobReferences = append(blobReferences, &blobReference{ blobReferences = append(blobReferences, &blobReference{
Digest: manifest.Config.Digest, Digest: manifest.Config.Digest,
MediaType: manifest.Config.MediaType, MediaType: manifest.Config.MediaType,
@ -388,19 +367,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
return nil, err return nil, err
} }
} else { } else {
props, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged) if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil {
if err != nil {
return nil, err 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 { 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 return nil, err
} }
} }

View File

@ -4,6 +4,8 @@
package user package user
import ( import (
gocontext "context"
"errors"
"net/http" "net/http"
"net/url" "net/url"
@ -20,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
alpine_module "code.gitea.io/gitea/modules/packages/alpine" alpine_module "code.gitea.io/gitea/modules/packages/alpine"
arch_module "code.gitea.io/gitea/modules/packages/arch" 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" debian_module "code.gitea.io/gitea/modules/packages/debian"
rpm_module "code.gitea.io/gitea/modules/packages/rpm" rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -31,6 +34,7 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
) )
const ( const (
@ -162,6 +166,24 @@ func RedirectToLastVersion(ctx *context.Context) {
ctx.Redirect(pd.VersionWebLink()) 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 // ViewPackageVersion displays a single package version
func ViewPackageVersion(ctx *context.Context) { func ViewPackageVersion(ctx *context.Context) {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
@ -169,6 +191,7 @@ func ViewPackageVersion(ctx *context.Context) {
return return
} }
versionSub := ctx.PathParam("version_sub")
pd := ctx.Package.Descriptor pd := ctx.Package.Descriptor
ctx.Data["Title"] = pd.Package.Name ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true ctx.Data["IsPackagesPage"] = true
@ -180,6 +203,9 @@ func ViewPackageVersion(ctx *context.Context) {
} }
ctx.Data["PackageRegistryHost"] = registryHostURL.Host ctx.Data["PackageRegistryHost"] = registryHostURL.Host
var pvs []*packages_model.PackageVersion
pvsTotal := int64(0)
switch pd.Package.Type { switch pd.Package.Type {
case packages_model.TypeAlpine: case packages_model.TypeAlpine:
branches := make(container.Set[string]) branches := make(container.Set[string])
@ -257,21 +283,26 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["Groups"] = util.Sorted(groups.Values()) ctx.Data["Groups"] = util.Sorted(groups.Values())
ctx.Data["Architectures"] = util.Sorted(architectures.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values())
}
var (
total int64
pvs []*packages_model.PackageVersion
)
switch pd.Package.Type {
case packages_model.TypeContainer: 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), Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID, PackageID: pd.Package.ID,
IsTagged: true, IsTagged: true,
}) })
default: 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), Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID, PackageID: pd.Package.ID,
IsInternal: optional.Some(false), IsInternal: optional.Some(false),
@ -283,7 +314,7 @@ func ViewPackageVersion(ctx *context.Context) {
} }
ctx.Data["LatestVersions"] = pvs ctx.Data["LatestVersions"] = pvs
ctx.Data["TotalVersionCount"] = total ctx.Data["TotalVersionCount"] = pvsTotal
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()

View File

@ -1012,6 +1012,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/versions", user.ListPackageVersions) m.Get("/versions", user.ListPackageVersions)
m.Group("/{version}", func() { m.Group("/{version}", func() {
m.Get("", user.ViewPackageVersion) m.Get("", user.ViewPackageVersion)
m.Get("/{version_sub}", user.ViewPackageVersion)
m.Get("/files/{fileid}", user.DownloadPackageFile) m.Get("/files/{fileid}", user.DownloadPackageFile)
m.Group("/settings", func() { m.Group("/settings", func() {
m.Get("", user.PackageSettings) m.Get("", user.PackageSettings)

View File

@ -5,11 +5,17 @@ package container
import ( import (
"context" "context"
"io"
"strings" "strings"
packages_model "code.gitea.io/gitea/models/packages" 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" 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" 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 // UpdateRepositoryNames updates the repository name property for all packages of the specific owner
@ -22,7 +28,7 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner
newOwnerName = strings.ToLower(newOwnerName) newOwnerName = strings.ToLower(newOwnerName)
for _, p := range ps { 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 return err
} }
@ -33,3 +39,26 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner
return nil 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
}

View File

@ -599,6 +599,12 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (
return GetPackageBlobStream(ctx, pf, pb, nil) 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 // 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. // 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) { func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
@ -617,7 +623,7 @@ func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, p
} }
} }
if u == nil { if u == nil {
s, err = cs.Get(key) s, err = cs.OpenBlob(key)
} }
if err == nil { if err == nil {

View File

@ -49,7 +49,11 @@
{{/* "unknown/unknown" is attestation-manifest, so we should skip it */}} {{/* "unknown/unknown" is attestation-manifest, so we should skip it */}}
{{if ne .Platform "unknown/unknown"}} {{if ne .Platform "unknown/unknown"}}
<tr> <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>{{.Platform}}</td>
<td>{{FileSize .Size}}</td> <td>{{FileSize .Size}}</td>
</tr> </tr>
@ -65,12 +69,24 @@
{{.PackageDescriptor.Metadata.Description}} {{.PackageDescriptor.Metadata.Description}}
</div> </div>
{{end}} {{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"> <div class="ui attached segment tw-break-anywhere">
<table class="ui very basic compact table"> <table class="ui very basic compact table">
<tbody> <tbody>
{{range .PackageDescriptor.Metadata.ImageLayers}} {{range $imageMetadata.ImageLayers}}
<tr> <tr>
<td>{{.}}</td> <td>{{.}}</td>
</tr> </tr>
@ -79,7 +95,7 @@
</table> </table>
</div> </div>
{{end}} {{end}}
{{if .PackageDescriptor.Metadata.Labels}} {{if $imageMetadata.Labels}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.labels"}}</h4> <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.labels"}}</h4>
<div class="ui attached segment"> <div class="ui attached segment">
<table class="ui very basic compact table"> <table class="ui very basic compact table">
@ -90,7 +106,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range $key, $value := .PackageDescriptor.Metadata.Labels}} {{range $key, $value := $imageMetadata.Labels}}
<tr> <tr>
<td class="tw-align-top">{{$key}}</td> <td class="tw-align-top">{{$key}}</td>
<td class="tw-break-anywhere">{{$value}}</td> <td class="tw-break-anywhere">{{$value}}</td>

View File

@ -4,7 +4,7 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label> <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>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label> <label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label>

View File

@ -1,4 +1,5 @@
<div class="issue-title-header"> <div class="issue-title-header">
{{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}}
<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1> <h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
<div> <div>
{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} {{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
@ -74,7 +75,7 @@
<div class="ui relaxed list"> <div class="ui relaxed list">
{{range .PackageDescriptor.Files}} {{range .PackageDescriptor.Files}}
<div class="item"> <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> <span class="text small file-size">{{FileSize .Blob.Size}}</span>
</div> </div>
{{end}} {{end}}
@ -98,7 +99,7 @@
<div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> <div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
{{end}} {{end}}
{{if .CanWritePackages}} {{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}} {{end}}
</div> </div>
{{end}} {{end}}

View File

@ -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.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) 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{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference))
assert.ElementsMatch(t, []string{untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference))
assert.IsType(t, &container_module.Metadata{}, pd.Metadata) assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
metadata := pd.Metadata.(*container_module.Metadata) metadata := pd.Metadata.(*container_module.Metadata)