]> source.dussan.org Git - gitea.git/commitdiff
Add Docker /v2/_catalog endpoint (#20469)
authorKN4CK3R <admin@oldschoolhack.me>
Thu, 28 Jul 2022 03:59:39 +0000 (05:59 +0200)
committerGitHub <noreply@github.com>
Thu, 28 Jul 2022 03:59:39 +0000 (11:59 +0800)
* Added properties for packages.
* Fixed authenticate header format.
* Added _catalog endpoint.
* Check owner visibility.
* Extracted condition.
* Added test for _catalog.

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
21 files changed:
integrations/api_packages_container_test.go
integrations/api_packages_npm_test.go
models/migrations/migrations.go
models/migrations/v220.go [new file with mode: 0644]
models/packages/container/search.go
models/packages/descriptor.go
models/packages/package.go
models/packages/package_property.go
models/user/search.go
modules/packages/container/metadata.go
routers/api/packages/api.go
routers/api/packages/composer/api.go
routers/api/packages/composer/composer.go
routers/api/packages/container/blob.go
routers/api/packages/container/container.go
routers/api/packages/container/manifest.go
routers/api/packages/npm/api.go
routers/web/org/setting.go
routers/web/user/setting/profile.go
services/packages/container/cleanup.go
services/packages/packages.go

index bdb8e2e90e3931eebc019cb0cdc27ebef3120d97..5e073f313f7aeda9b645f3b6a0bd64f570a0303e 100644 (file)
@@ -27,6 +27,7 @@ import (
 
 func TestPackageContainer(t *testing.T) {
        defer prepareTestEnv(t)()
+
        user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 
        has := func(l packages_model.PackagePropertyList, name string) bool {
@@ -37,6 +38,15 @@ func TestPackageContainer(t *testing.T) {
                }
                return false
        }
+       getAllByName := func(l packages_model.PackagePropertyList, name string) []string {
+               values := make([]string, 0, len(l))
+               for _, pp := range l {
+                       if pp.Name == name {
+                               values = append(values, pp.Value)
+                       }
+               }
+               return values
+       }
 
        images := []string{"test", "te/st"}
        tags := []string{"latest", "main"}
@@ -67,7 +77,7 @@ func TestPackageContainer(t *testing.T) {
                        Token string `json:"token"`
                }
 
-               authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token"`}
+               authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
 
                t.Run("Anonymous", func(t *testing.T) {
                        defer PrintCurrentTest(t)()
@@ -237,7 +247,8 @@ func TestPackageContainer(t *testing.T) {
                                                assert.Nil(t, pd.SemVer)
                                                assert.Equal(t, image, pd.Package.Name)
                                                assert.Equal(t, tag, pd.Version.Version)
-                                               assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged))
+                                               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.IsType(t, &container_module.Metadata{}, pd.Metadata)
                                                metadata := pd.Metadata.(*container_module.Metadata)
@@ -331,7 +342,8 @@ func TestPackageContainer(t *testing.T) {
                                assert.Nil(t, pd.SemVer)
                                assert.Equal(t, image, pd.Package.Name)
                                assert.Equal(t, untaggedManifestDigest, pd.Version.Version)
-                               assert.False(t, has(pd.Properties, container_module.PropertyManifestTagged))
+                               assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
+                               assert.False(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
 
                                assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
 
@@ -363,18 +375,10 @@ func TestPackageContainer(t *testing.T) {
                                assert.Nil(t, pd.SemVer)
                                assert.Equal(t, image, pd.Package.Name)
                                assert.Equal(t, multiTag, pd.Version.Version)
-                               assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged))
+                               assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
+                               assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
 
-                               getAllByName := func(l packages_model.PackagePropertyList, name string) []string {
-                                       values := make([]string, 0, len(l))
-                                       for _, pp := range l {
-                                               if pp.Name == name {
-                                                       values = append(values, pp.Value)
-                                               }
-                                       }
-                                       return values
-                               }
-                               assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.Properties, 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)
@@ -536,4 +540,56 @@ func TestPackageContainer(t *testing.T) {
                        })
                })
        }
+
+       t.Run("OwnerNameChange", func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+
+               checkCatalog := func(owner string) func(t *testing.T) {
+                       return func(t *testing.T) {
+                               defer PrintCurrentTest(t)()
+
+                               req := NewRequest(t, "GET", fmt.Sprintf("%sv2/_catalog", setting.AppURL))
+                               addTokenAuthHeader(req, userToken)
+                               resp := MakeRequest(t, req, http.StatusOK)
+
+                               type RepositoryList struct {
+                                       Repositories []string `json:"repositories"`
+                               }
+
+                               repoList := &RepositoryList{}
+                               DecodeJSON(t, resp, &repoList)
+
+                               assert.Len(t, repoList.Repositories, len(images))
+                               names := make([]string, 0, len(images))
+                               for _, image := range images {
+                                       names = append(names, strings.ToLower(owner+"/"+image))
+                               }
+                               assert.ElementsMatch(t, names, repoList.Repositories)
+                       }
+               }
+
+               t.Run(fmt.Sprintf("Catalog[%s]", user.LowerName), checkCatalog(user.LowerName))
+
+               session := loginUser(t, user.Name)
+
+               newOwnerName := "newUsername"
+
+               req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+                       "_csrf":    GetCSRF(t, session, "/user/settings"),
+                       "name":     newOwnerName,
+                       "email":    "user2@example.com",
+                       "language": "en-US",
+               })
+               session.MakeRequest(t, req, http.StatusSeeOther)
+
+               t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
+
+               req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+                       "_csrf":    GetCSRF(t, session, "/user/settings"),
+                       "name":     user.Name,
+                       "email":    "user2@example.com",
+                       "language": "en-US",
+               })
+               session.MakeRequest(t, req, http.StatusSeeOther)
+       })
 }
index 28a371193982831e47d11b87825626dd1dc1f520..ad88ac5da6764dd59fbe8e8f74bd258eeae4a8ee 100644 (file)
@@ -85,9 +85,9 @@ func TestPackageNpm(t *testing.T) {
                assert.IsType(t, &npm.Metadata{}, pd.Metadata)
                assert.Equal(t, packageName, pd.Package.Name)
                assert.Equal(t, packageVersion, pd.Version.Version)
-               assert.Len(t, pd.Properties, 1)
-               assert.Equal(t, npm.TagProperty, pd.Properties[0].Name)
-               assert.Equal(t, packageTag, pd.Properties[0].Value)
+               assert.Len(t, pd.VersionProperties, 1)
+               assert.Equal(t, npm.TagProperty, pd.VersionProperties[0].Name)
+               assert.Equal(t, packageTag, pd.VersionProperties[0].Value)
 
                pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
                assert.NoError(t, err)
index 1b2a743b6d357f84f348dd97295dce623db7c3df..beeba866dc70f817f3ca1a0625223e04464fe335 100644 (file)
@@ -398,6 +398,8 @@ var migrations = []Migration{
        NewMigration("Improve Action table indices v2", improveActionTableIndices),
        // v219 -> v220
        NewMigration("Add sync_on_commit column to push_mirror table", addSyncOnCommitColForPushMirror),
+       // v220 -> v221
+       NewMigration("Add container repository property", addContainerRepositoryProperty),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v220.go b/models/migrations/v220.go
new file mode 100644 (file)
index 0000000..f598358
--- /dev/null
@@ -0,0 +1,29 @@
+// 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 migrations
+
+import (
+       packages_model "code.gitea.io/gitea/models/packages"
+       container_module "code.gitea.io/gitea/modules/packages/container"
+
+       "xorm.io/xorm"
+       "xorm.io/xorm/schemas"
+)
+
+func addContainerRepositoryProperty(x *xorm.Engine) error {
+       switch x.Dialect().URI().DBType {
+       case schemas.SQLITE:
+               _, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, u.lower_name || '/' || p.lower_name FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
+               if err != nil {
+                       return err
+               }
+       default:
+               _, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, CONCAT(u.lower_name, '/', p.lower_name) FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
+               if err != nil {
+                       return err
+               }
+       }
+       return nil
+}
index 972cac9528f860b0222882908936a1ba0eab89a4..a3409fe7431175a2d83cd5927df747d94b21328e 100644 (file)
@@ -12,6 +12,7 @@ import (
 
        "code.gitea.io/gitea/models/db"
        "code.gitea.io/gitea/models/packages"
+       user_model "code.gitea.io/gitea/models/user"
        container_module "code.gitea.io/gitea/modules/packages/container"
 
        "xorm.io/builder"
@@ -210,6 +211,7 @@ func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*pack
        return pvs, count, err
 }
 
+// SearchExpiredUploadedBlobs gets all uploaded blobs which are older than specified
 func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([]*packages.PackageFile, error) {
        var cond builder.Cond = builder.Eq{
                "package_version.is_internal":   true,
@@ -225,3 +227,37 @@ func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([
                Where(cond).
                Find(&pfs)
 }
+
+// GetRepositories gets a sorted list of all repositories
+func GetRepositories(ctx context.Context, actor *user_model.User, n int, last string) ([]string, error) {
+       var cond builder.Cond = builder.Eq{
+               "package.type":              packages.TypeContainer,
+               "package_property.ref_type": packages.PropertyTypePackage,
+               "package_property.name":     container_module.PropertyRepository,
+       }
+
+       cond = cond.And(builder.Exists(
+               builder.
+                       Select("package_version.id").
+                       Where(builder.Eq{"package_version.is_internal": false}.And(builder.Expr("package.id = package_version.package_id"))).
+                       From("package_version"),
+       ))
+
+       if last != "" {
+               cond = cond.And(builder.Gt{"package_property.value": strings.ToLower(last)})
+       }
+
+       cond = cond.And(user_model.BuildCanSeeUserCondition(actor))
+
+       sess := db.GetEngine(ctx).
+               Table("package").
+               Select("package_property.value").
+               Join("INNER", "user", "`user`.id = package.owner_id").
+               Join("INNER", "package_property", "package_property.ref_id = package.id").
+               Where(cond).
+               Asc("package_property.value").
+               Limit(n)
+
+       repositories := make([]string, 0, n)
+       return repositories, sess.Find(&repositories)
+}
index fbdc40f37fbb2afe0547f11cd3fc4f2f3ee8338a..31819ccca1abe2c215401b88eac19aebceaa62fc 100644 (file)
@@ -40,15 +40,16 @@ func (l PackagePropertyList) GetByName(name string) string {
 
 // PackageDescriptor describes a package
 type PackageDescriptor struct {
-       Package    *Package
-       Owner      *user_model.User
-       Repository *repo_model.Repository
-       Version    *PackageVersion
-       SemVer     *version.Version
-       Creator    *user_model.User
-       Properties PackagePropertyList
-       Metadata   interface{}
-       Files      []*PackageFileDescriptor
+       Package           *Package
+       Owner             *user_model.User
+       Repository        *repo_model.Repository
+       Version           *PackageVersion
+       SemVer            *version.Version
+       Creator           *user_model.User
+       PackageProperties PackagePropertyList
+       VersionProperties PackagePropertyList
+       Metadata          interface{}
+       Files             []*PackageFileDescriptor
 }
 
 // PackageFileDescriptor describes a package file
@@ -102,6 +103,10 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
                        return nil, err
                }
        }
+       pps, err := GetProperties(ctx, PropertyTypePackage, p.ID)
+       if err != nil {
+               return nil, err
+       }
        pvps, err := GetProperties(ctx, PropertyTypeVersion, pv.ID)
        if err != nil {
                return nil, err
@@ -152,15 +157,16 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
        }
 
        return &PackageDescriptor{
-               Package:    p,
-               Owner:      o,
-               Repository: repository,
-               Version:    pv,
-               SemVer:     semVer,
-               Creator:    creator,
-               Properties: PackagePropertyList(pvps),
-               Metadata:   metadata,
-               Files:      pfds,
+               Package:           p,
+               Owner:             o,
+               Repository:        repository,
+               Version:           pv,
+               SemVer:            semVer,
+               Creator:           creator,
+               PackageProperties: PackagePropertyList(pps),
+               VersionProperties: PackagePropertyList(pvps),
+               Metadata:          metadata,
+               Files:             pfds,
        }, nil
 }
 
index bdb535492bb408ab3d6f6d704ff884baa3151f22..97cfbc6cad20e11125c013bcaca00cfe09411c2d 100644 (file)
@@ -131,6 +131,12 @@ func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) {
        return p, nil
 }
 
+// DeletePackageByID deletes a package by id
+func DeletePackageByID(ctx context.Context, packageID int64) error {
+       _, err := db.GetEngine(ctx).ID(packageID).Delete(&Package{})
+       return err
+}
+
 // SetRepositoryLink sets the linked repository
 func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error {
        _, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: repoID})
@@ -192,21 +198,20 @@ func GetPackagesByType(ctx context.Context, ownerID int64, packageType Type) ([]
                Find(&ps)
 }
 
-// DeletePackagesIfUnreferenced deletes a package if there are no associated versions
-func DeletePackagesIfUnreferenced(ctx context.Context) error {
+// FindUnreferencedPackages gets all packages without associated versions
+func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) {
        in := builder.
                Select("package.id").
                From("package").
                LeftJoin("package_version", "package_version.package_id = package.id").
                Where(builder.Expr("package_version.id IS NULL"))
 
-       _, err := db.GetEngine(ctx).
+       ps := make([]*Package, 0, 10)
+       return ps, db.GetEngine(ctx).
                // double select workaround for MySQL
                // https://stackoverflow.com/questions/4471277/mysql-delete-from-with-subquery-as-condition
                Where(builder.In("package.id", builder.Select("id").From(in, "temp"))).
-               Delete(&Package{})
-
-       return err
+               Find(&ps)
 }
 
 // HasOwnerPackages tests if a user/org has packages
index bf7dc346c6c975e8e532243c27e5f2fd883a84f9..fc107138019471d5ea4aecf1aa1d80c538ed0585 100644 (file)
@@ -21,9 +21,11 @@ const (
        PropertyTypeVersion PropertyType = iota // 0
        // PropertyTypeFile means the reference is a package file
        PropertyTypeFile // 1
+       // PropertyTypePackage means the reference is a package
+       PropertyTypePackage // 2
 )
 
-// PackageProperty represents a property of a package version or file
+// PackageProperty represents a property of a package, version or file
 type PackageProperty struct {
        ID      int64        `xorm:"pk autoincr"`
        RefType PropertyType `xorm:"INDEX NOT NULL"`
@@ -68,3 +70,9 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error {
        _, err := db.GetEngine(ctx).ID(propertyID).Delete(&PackageProperty{})
        return err
 }
+
+// DeletePropertyByName deletes properties by name
+func DeletePropertyByName(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
+}
index 76ff55ea2664eae99b4b39aea267bc8f952749ca..f8e6c89f06804a10823b2a3d48f3471cf80a3a46 100644 (file)
@@ -58,24 +58,7 @@ func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session {
                cond = cond.And(builder.In("visibility", opts.Visible))
        }
 
-       if opts.Actor != nil {
-               // If Admin - they see all users!
-               if !opts.Actor.IsAdmin {
-                       // Users can see an organization they are a member of
-                       accessCond := builder.In("id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": opts.Actor.ID}))
-                       if !opts.Actor.IsRestricted {
-                               // Not-Restricted users can see public and limited users/organizations
-                               accessCond = accessCond.Or(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
-                       }
-                       // Don't forget about self
-                       accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
-                       cond = cond.And(accessCond)
-               }
-       } else {
-               // Force visibility for privacy
-               // Not logged in - only public users
-               cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
-       }
+       cond = cond.And(BuildCanSeeUserCondition(opts.Actor))
 
        if opts.UID > 0 {
                cond = cond.And(builder.Eq{"id": opts.UID})
@@ -163,3 +146,26 @@ func IterateUser(f func(user *User) error) error {
                }
        }
 }
+
+// BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see
+func BuildCanSeeUserCondition(actor *User) builder.Cond {
+       if actor != nil {
+               // If Admin - they see all users!
+               if !actor.IsAdmin {
+                       // Users can see an organization they are a member of
+                       cond := builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID}))
+                       if !actor.IsRestricted {
+                               // Not-Restricted users can see public and limited users/organizations
+                               cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
+                       }
+                       // Don't forget about self
+                       return cond.Or(builder.Eq{"`user`.id": actor.ID})
+               }
+
+               return nil
+       }
+
+       // Force visibility for privacy
+       // Not logged in - only public users
+       return builder.In("`user`.visibility", structs.VisibleTypePublic)
+}
index 087d38e5bd1425b6bacdd58949205dedc082cec4..4222cdb30a7812c2ebe050b1f12d9c74d45785dd 100644 (file)
@@ -16,6 +16,7 @@ import (
 )
 
 const (
+       PropertyRepository        = "container.repository"
        PropertyDigest            = "container.digest"
        PropertyMediaType         = "container.mediatype"
        PropertyManifestTagged    = "container.manifest.tagged"
index b5fdc739d7c1085de5461f981efda66252884f3d..bb9a42e33dc9928f67f5fb9f0778c31f406550c4 100644 (file)
@@ -257,6 +257,7 @@ func ContainerRoutes() *web.Route {
 
        r.Get("", container.ReqContainerAccess, container.DetermineSupport)
        r.Get("/token", container.Authenticate)
+       r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
        r.Group("/{username}", func() {
                r.Group("/{image}", func() {
                        r.Group("/blobs/uploads", func() {
index 5e1cc293da0f84c6800edb502c3f933f39d9dff6..45bb7eae1c1979236c246c26999f35744bccdbdc 100644 (file)
@@ -88,7 +88,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
 
        for _, pd := range pds {
                packageType := ""
-               for _, pvp := range pd.Properties {
+               for _, pvp := range pd.VersionProperties {
                        if pvp.Name == composer_module.TypeProperty {
                                packageType = pvp.Value
                                break
index b7c1f140dcf4934a5e850870488e9ac99dfddfaf..81cef39f1c4969c37736dcecddce9e358d2e959a 100644 (file)
@@ -227,7 +227,7 @@ func UploadPackage(ctx *context.Context) {
                        SemverCompatible: true,
                        Creator:          ctx.Doer,
                        Metadata:         cp.Metadata,
-                       Properties: map[string]string{
+                       VersionProperties: map[string]string{
                                composer_module.TypeProperty: cp.Type,
                        },
                },
index 8f6254f583213392afcce3a7fb3b64cd8965c116..8a9cbd4a15fbf77b6d86a585e5ffc55c41386423 100644 (file)
@@ -29,6 +29,7 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
        contentStore := packages_module.NewContentStore()
 
        err := db.WithTx(func(ctx context.Context) error {
+               created := true
                p := &packages_model.Package{
                        OwnerID:   pi.Owner.ID,
                        Type:      packages_model.TypeContainer,
@@ -37,12 +38,21 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
                }
                var err error
                if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
-                       if err != packages_model.ErrDuplicatePackage {
+                       if err == packages_model.ErrDuplicatePackage {
+                               created = false
+                       } else {
                                log.Error("Error inserting package: %v", err)
                                return err
                        }
                }
 
+               if created {
+                       if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil {
+                               log.Error("Error setting package property: %v", err)
+                               return err
+                       }
+               }
+
                pv := &packages_model.PackageVersion{
                        PackageID:    p.ID,
                        CreatorID:    pi.Owner.ID,
index 2a564b3446a16d3f97a55cd1863eed8680b59c9b..b961cd4afb393a7ddf6f0d6be741b2c14a72e2ca 100644 (file)
@@ -112,7 +112,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
 // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
 func ReqContainerAccess(ctx *context.Context) {
        if ctx.Doer == nil {
-               ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token"`)
+               ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
                apiErrorDefined(ctx, errUnauthorized)
        }
 }
@@ -151,6 +151,39 @@ func Authenticate(ctx *context.Context) {
        })
 }
 
+// https://docs.docker.com/registry/spec/api/#listing-repositories
+func GetRepositoryList(ctx *context.Context) {
+       n := ctx.FormInt("n")
+       if n <= 0 || n > 100 {
+               n = 100
+       }
+       last := ctx.FormTrim("last")
+
+       repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
+       if err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       type RepositoryList struct {
+               Repositories []string `json:"repositories"`
+       }
+
+       if len(repositories) == n {
+               v := url.Values{}
+               if n > 0 {
+                       v.Add("n", strconv.Itoa(n))
+               }
+               v.Add("last", repositories[len(repositories)-1])
+
+               ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
+       }
+
+       jsonResponse(ctx, http.StatusOK, RepositoryList{
+               Repositories: repositories,
+       })
+}
+
 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
index d899ac8ee2f6a201c51d67deaee7f429bb745163..319c9bcabc11c135b3d58e01ea5583206369cda5 100644 (file)
@@ -267,6 +267,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H
 }
 
 func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
+       created := true
        p := &packages_model.Package{
                OwnerID:   mci.Owner.ID,
                Type:      packages_model.TypeContainer,
@@ -275,12 +276,21 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
        }
        var err error
        if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
-               if err != packages_model.ErrDuplicatePackage {
+               if err == packages_model.ErrDuplicatePackage {
+                       created = false
+               } else {
                        log.Error("Error inserting package: %v", err)
                        return nil, err
                }
        }
 
+       if created {
+               if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil {
+                       log.Error("Error setting package property: %v", err)
+                       return nil, err
+               }
+       }
+
        metadata.IsTagged = mci.IsTagged
 
        metadataJSON, err := json.Marshal(metadata)
index 56c897704398d1fbc840b3d9d101f16c00efc370..4b6b803971b7a8960e82e08a8d78da051e97bd96 100644 (file)
@@ -25,7 +25,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
        for _, pd := range pds {
                versions[pd.SemVer.String()] = createPackageMetadataVersion(registryURL, pd)
 
-               for _, pvp := range pd.Properties {
+               for _, pvp := range pd.VersionProperties {
                        if pvp.Name == npm_module.TagProperty {
                                distTags[pvp.Value] = pd.Version.Version
                        }
index c22a124e7421af98498b1699f774635202e30a43..3f7bc59856f3660e35163bfe5519b2ae7ec9792e 100644 (file)
@@ -24,6 +24,7 @@ import (
        user_setting "code.gitea.io/gitea/routers/web/user/setting"
        "code.gitea.io/gitea/services/forms"
        "code.gitea.io/gitea/services/org"
+       container_service "code.gitea.io/gitea/services/packages/container"
        repo_service "code.gitea.io/gitea/services/repository"
        user_service "code.gitea.io/gitea/services/user"
 )
@@ -88,6 +89,12 @@ func SettingsPost(ctx *context.Context) {
                        }
                        return
                }
+
+               if err := container_service.UpdateRepositoryNames(ctx, org.AsUser(), form.Name); err != nil {
+                       ctx.ServerError("UpdateRepositoryNames", err)
+                       return
+               }
+
                // reset ctx.org.OrgLink with new name
                ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
                log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
index b07813e7250e67b1b6345942066c64a21591eecd..c9a7afe982f78e3ac268422e9b52bbb511f630b6 100644 (file)
@@ -30,6 +30,7 @@ import (
        "code.gitea.io/gitea/modules/web/middleware"
        "code.gitea.io/gitea/services/agit"
        "code.gitea.io/gitea/services/forms"
+       container_service "code.gitea.io/gitea/services/packages/container"
        user_service "code.gitea.io/gitea/services/user"
 )
 
@@ -90,6 +91,11 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s
                return err
        }
 
+       if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil {
+               ctx.ServerError("UpdateRepositoryNames", err)
+               return err
+       }
+
        log.Trace("User name changed: %s -> %s", user.Name, newName)
        return nil
 }
index 3e44f9aa1a0f625756d4a9925f78a02f38cc81af..d23a481f279e4f907a9251908401c5654a9ab014 100644 (file)
@@ -6,10 +6,13 @@ package container
 
 import (
        "context"
+       "strings"
        "time"
 
        packages_model "code.gitea.io/gitea/models/packages"
        container_model "code.gitea.io/gitea/models/packages/container"
+       user_model "code.gitea.io/gitea/models/user"
+       container_module "code.gitea.io/gitea/modules/packages/container"
        "code.gitea.io/gitea/modules/util"
 )
 
@@ -78,3 +81,25 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
 
        return nil
 }
+
+// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
+func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error {
+       ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer)
+       if err != nil {
+               return err
+       }
+
+       newOwnerName = strings.ToLower(newOwnerName)
+
+       for _, p := range ps {
+               if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
+                       return err
+               }
+
+               if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, newOwnerName+"/"+p.LowerName); err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
index aa1796e8b3f78c7dcb85bd167a8e9b96dab59cad..975c5ddd358904ed1ea1d9936db90703a09df098 100644 (file)
@@ -34,10 +34,11 @@ type PackageInfo struct {
 // PackageCreationInfo describes a package to create
 type PackageCreationInfo struct {
        PackageInfo
-       SemverCompatible bool
-       Creator          *user_model.User
-       Metadata         interface{}
-       Properties       map[string]string
+       SemverCompatible  bool
+       Creator           *user_model.User
+       Metadata          interface{}
+       PackageProperties map[string]string
+       VersionProperties map[string]string
 }
 
 // PackageFileInfo describes a package file
@@ -110,8 +111,9 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio
 }
 
 func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, bool, error) {
-       log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.Properties, allowDuplicate)
+       log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.PackageProperties, pvci.VersionProperties, allowDuplicate)
 
+       packageCreated := true
        p := &packages_model.Package{
                OwnerID:          pvci.Owner.ID,
                Type:             pvci.PackageType,
@@ -121,18 +123,29 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
        }
        var err error
        if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
-               if err != packages_model.ErrDuplicatePackage {
+               if err == packages_model.ErrDuplicatePackage {
+                       packageCreated = false
+               } else {
                        log.Error("Error inserting package: %v", err)
                        return nil, false, err
                }
        }
 
+       if packageCreated {
+               for name, value := range pvci.PackageProperties {
+                       if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, name, value); err != nil {
+                               log.Error("Error setting package property: %v", err)
+                               return nil, false, err
+                       }
+               }
+       }
+
        metadataJSON, err := json.Marshal(pvci.Metadata)
        if err != nil {
                return nil, false, err
        }
 
-       created := true
+       versionCreated := true
        pv := &packages_model.PackageVersion{
                PackageID:    p.ID,
                CreatorID:    pvci.Creator.ID,
@@ -142,7 +155,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
        }
        if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
                if err == packages_model.ErrDuplicatePackageVersion {
-                       created = false
+                       versionCreated = false
                }
                if err != packages_model.ErrDuplicatePackageVersion || !allowDuplicate {
                        log.Error("Error inserting package: %v", err)
@@ -150,8 +163,8 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
                }
        }
 
-       if created {
-               for name, value := range pvci.Properties {
+       if versionCreated {
+               for name, value := range pvci.VersionProperties {
                        if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
                                log.Error("Error setting package version property: %v", err)
                                return nil, false, err
@@ -159,7 +172,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
                }
        }
 
-       return pv, created, nil
+       return pv, versionCreated, nil
 }
 
 // AddFileToExistingPackage adds a file to an existing package. If the package does not exist, ErrPackageNotExist is returned
@@ -350,9 +363,18 @@ func Cleanup(unused context.Context, olderThan time.Duration) error {
                return err
        }
 
-       if err := packages_model.DeletePackagesIfUnreferenced(ctx); err != nil {
+       ps, err := packages_model.FindUnreferencedPackages(ctx)
+       if err != nil {
                return err
        }
+       for _, p := range ps {
+               if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil {
+                       return err
+               }
+               if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
+                       return err
+               }
+       }
 
        pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan)
        if err != nil {