From 97a8c96c5b45123f580244edbc9b2ad3e102b6ee Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sat, 30 Jul 2022 17:52:04 +0200 Subject: [PATCH] Add Docker /v2/_catalog endpoint (#20469) (#20556) * 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: KN4CK3R Co-authored-by: Lunny Xiao Co-authored-by: Lauris BH Co-authored-by: wxiaoguang --- integrations/api_packages_container_test.go | 84 +++++++++++++++++---- integrations/api_packages_npm_test.go | 6 +- models/migrations/migrations.go | 4 + models/migrations/v219.go | 31 ++++++++ models/migrations/v220.go | 29 +++++++ models/packages/container/search.go | 36 +++++++++ models/packages/descriptor.go | 42 ++++++----- models/packages/package.go | 17 +++-- models/packages/package_property.go | 10 ++- models/user/search.go | 49 ++++++------ modules/packages/container/metadata.go | 1 + routers/api/packages/api.go | 1 + routers/api/packages/composer/api.go | 2 +- routers/api/packages/composer/composer.go | 2 +- routers/api/packages/container/blob.go | 12 ++- routers/api/packages/container/container.go | 35 ++++++++- routers/api/packages/container/manifest.go | 12 ++- routers/api/packages/npm/api.go | 2 +- routers/web/org/setting.go | 7 ++ routers/web/user/setting/profile.go | 6 ++ services/packages/container/cleanup.go | 25 ++++++ services/packages/packages.go | 46 ++++++++--- 22 files changed, 374 insertions(+), 85 deletions(-) create mode 100644 models/migrations/v219.go create mode 100644 models/migrations/v220.go diff --git a/integrations/api_packages_container_test.go b/integrations/api_packages_container_test.go index 2b5be9dd4c..850ac626f6 100644 --- a/integrations/api_packages_container_test.go +++ b/integrations/api_packages_container_test.go @@ -26,6 +26,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 { @@ -36,6 +37,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"} @@ -66,7 +76,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)() @@ -236,7 +246,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) @@ -330,7 +341,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) @@ -362,18 +374,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) @@ -528,4 +532,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) + }) } diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index 28a3711939..ad88ac5da6 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -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) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 0d35ac78d3..beeba866dc 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -396,6 +396,10 @@ var migrations = []Migration{ NewMigration("Alter hook_task table TEXT fields to LONGTEXT", alterHookTaskTextFieldsToLongText), // v218 -> v219 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/v219.go b/models/migrations/v219.go new file mode 100644 index 0000000000..7b4f34b704 --- /dev/null +++ b/models/migrations/v219.go @@ -0,0 +1,31 @@ +// 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 ( + "time" + + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addSyncOnCommitColForPushMirror(x *xorm.Engine) error { + type PushMirror struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + Repo *repo.Repository `xorm:"-"` + RemoteName string + + SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"` + Interval time.Duration + CreatedUnix timeutil.TimeStamp `xorm:"created"` + LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"` + LastError string `xorm:"text"` + } + + return x.Sync2(new(PushMirror)) +} diff --git a/models/migrations/v220.go b/models/migrations/v220.go new file mode 100644 index 0000000000..f5983582a3 --- /dev/null +++ b/models/migrations/v220.go @@ -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 +} diff --git a/models/packages/container/search.go b/models/packages/container/search.go index 972cac9528..a3409fe743 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -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) +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index fbdc40f37f..31819ccca1 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -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 } diff --git a/models/packages/package.go b/models/packages/package.go index bdb535492b..97cfbc6cad 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -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 diff --git a/models/packages/package_property.go b/models/packages/package_property.go index bf7dc346c6..fc10713801 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -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 +} diff --git a/models/user/search.go b/models/user/search.go index a81cee1c22..f8e6c89f06 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -58,31 +58,7 @@ func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session { cond = cond.And(builder.In("visibility", opts.Visible)) } - if opts.Actor != nil { - var exprCond builder.Cond = builder.Expr("org_user.org_id = `user`.id") - - // If Admin - they see all users! - if !opts.Actor.IsAdmin { - // Force visibility for privacy - var accessCond builder.Cond - if !opts.Actor.IsRestricted { - accessCond = builder.Or( - builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))), - builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) - } else { - // restricted users only see orgs they are a member of - accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) - } - // 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}) @@ -170,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) +} diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index 087d38e5bd..4222cdb30a 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -16,6 +16,7 @@ import ( ) const ( + PropertyRepository = "container.repository" PropertyDigest = "container.digest" PropertyMediaType = "container.mediatype" PropertyManifestTagged = "container.manifest.tagged" diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index b5fdc739d7..bb9a42e33d 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -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() { diff --git a/routers/api/packages/composer/api.go b/routers/api/packages/composer/api.go index d8f67d130c..468c6aaaaa 100644 --- a/routers/api/packages/composer/api.go +++ b/routers/api/packages/composer/api.go @@ -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 diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 23de28c7f9..d0b2a50aad 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -225,7 +225,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, }, }, diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 8f6254f583..8a9cbd4a15 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -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, diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 2a564b3446..b961cd4afb 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -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(`; 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 diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index d899ac8ee2..319c9bcabc 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -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) diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index 56c8977043..4b6b803971 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -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 } diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index c22a124e74..3f7bc59856 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -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) diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 972271269f..a56bd63ee9 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -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 } diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index 390a0b7b05..920218b548 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -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 +} diff --git a/services/packages/packages.go b/services/packages/packages.go index 7f25fce5b8..312f7812b3 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -32,10 +32,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 @@ -108,8 +109,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, @@ -119,18 +121,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, @@ -140,7 +153,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) @@ -148,8 +161,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 @@ -157,7 +170,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 @@ -348,9 +361,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 {