mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-02-19 01:47:58 -05:00
Auto-link container images to repository (#10617)
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Implements auto-linking container images from the package registry to a repository (closes #2823). This might ease implementing #2699 in the future. Specifically, auto-linking happens on package creation and NOT when publishing updates to the same package. This should prevent "relinking" a manually unlinked package when publishing an update. Linking is performed either via the the Docker label `` (as described here: https://codeberg.org/forgejo/forgejo/issues/2823#issuecomment-8163866) or by naming the image like the repository (supports nested image names). ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - ~~[ ] in their respective `*_test.go` for unit tests.~~ _(Not required, since only already tested functions were used)_ - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - ~~I added test coverage for JavaScript changes...~~ _(No changes to JavaScript code)_ ### Documentation - [X] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change: https://codeberg.org/forgejo/docs/pulls/1666 - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [X] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10617 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: Leon Schmidt <mail@leon.wtf> Co-committed-by: Leon Schmidt <mail@leon.wtf>
This commit is contained in:
parent
8cca9317c2
commit
fda54d59b8
4 changed files with 351 additions and 12 deletions
|
|
@ -16,11 +16,12 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
PropertyRepository = "container.repository"
|
||||
PropertyDigest = "container.digest"
|
||||
PropertyMediaType = "container.mediatype"
|
||||
PropertyManifestTagged = "container.manifest.tagged"
|
||||
PropertyManifestReference = "container.manifest.reference"
|
||||
PropertyRepository = "container.repository"
|
||||
PropertyRepositoryAutolinkingPending = "container.repository.autolinking-pending"
|
||||
PropertyDigest = "container.digest"
|
||||
PropertyMediaType = "container.mediatype"
|
||||
PropertyManifestTagged = "container.manifest.tagged"
|
||||
PropertyManifestReference = "container.manifest.reference"
|
||||
|
||||
DefaultPlatform = "linux/amd64"
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ type Metadata struct {
|
|||
Labels map[string]string `json:"labels,omitempty"`
|
||||
ImageLayers []string `json:"layer_creation,omitempty"`
|
||||
Manifests []*Manifest `json:"manifests,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
type Manifest struct {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
|
|||
LowerName: strings.ToLower(pi.Name),
|
||||
}
|
||||
var err error
|
||||
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackage {
|
||||
created = false
|
||||
|
|
@ -116,7 +117,11 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
|
|||
|
||||
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)
|
||||
log.Error("Error setting package property %s: %v", container_module.PropertyRepository, err)
|
||||
return err
|
||||
}
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepositoryAutolinkingPending, "yes"); err != nil {
|
||||
log.Error("Error setting package property %s: %v", container_module.PropertyRepositoryAutolinkingPending, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,20 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
packages_model "forgejo.org/models/packages"
|
||||
container_model "forgejo.org/models/packages/container"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/json"
|
||||
"forgejo.org/modules/log"
|
||||
packages_module "forgejo.org/modules/packages"
|
||||
container_module "forgejo.org/modules/packages/container"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/util"
|
||||
notify_service "forgejo.org/services/notify"
|
||||
packages_service "forgejo.org/services/packages"
|
||||
|
|
@ -117,6 +120,7 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata.Annotations = manifest.Annotations
|
||||
|
||||
blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))
|
||||
|
||||
|
|
@ -320,6 +324,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
|||
LowerName: strings.ToLower(mci.Image),
|
||||
}
|
||||
var err error
|
||||
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackage {
|
||||
created = false
|
||||
|
|
@ -331,9 +336,32 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
|||
|
||||
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)
|
||||
log.Error("Error setting package property %s: %v", container_module.PropertyRepository, err)
|
||||
return nil, err
|
||||
}
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepositoryAutolinkingPending, "yes"); err != nil {
|
||||
log.Error("Error setting package property %s: %v", container_module.PropertyRepositoryAutolinkingPending, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if auto-linking is required (this only happens after creation of package (not version!))
|
||||
autolinkRequiredProps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepositoryAutolinkingPending)
|
||||
if err != nil {
|
||||
log.Error("Error getting package properties %s: %v", container_module.PropertyRepositoryAutolinkingPending, err)
|
||||
return nil, err
|
||||
}
|
||||
if len(autolinkRequiredProps) > 0 {
|
||||
autolinkRequiredProp := autolinkRequiredProps[0]
|
||||
if autolinkRequiredProp != nil && autolinkRequiredProp.Value == "yes" { // check if auto-link is required (this prevents re-auto-linking on new versions, since the property is not set there)
|
||||
if _, err := tryAutoLink(ctx, p, mci.Owner.LowerName, mci.Image, metadata, mci.Creator); err != nil {
|
||||
log.Error("Auto-linking failed for package %d: %v", p.ID, err)
|
||||
}
|
||||
// remove property regardless of success/failure to keep behavior consistent and prevent retries on re-runs.
|
||||
if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepositoryAutolinkingPending); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadata.IsTagged = mci.IsTagged
|
||||
|
|
@ -481,3 +509,98 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack
|
|||
|
||||
return pb, !exists, manifestDigest, err
|
||||
}
|
||||
|
||||
// Attempty to link a package to a repository in the following order of precedence: by annotation, by label and finally by image name.
|
||||
// If it fails, it returns false, nil. Only actual errors are returned, so don't use the err return only to determine if the linking was performed.
|
||||
func tryAutoLink(ctx context.Context, p *packages_model.Package, imageOwner, imageName string, metadata *container_module.Metadata, doer *user_model.User) (linked bool, err error) {
|
||||
// We can use the same function for linking by annotation as is used for
|
||||
// linking by label, since the field has the exact same structure
|
||||
if linkedByAnnotation, err := tryAutolinkByLabel(ctx, p, metadata.Annotations, doer); err != nil {
|
||||
return false, err
|
||||
} else if linkedByAnnotation {
|
||||
log.Info("Image %s/%s was auto-linked by annotation", imageOwner, imageName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if linkedByLabel, err := tryAutolinkByLabel(ctx, p, metadata.Labels, doer); err != nil {
|
||||
return false, err
|
||||
} else if linkedByLabel {
|
||||
log.Info("Image %s/%s was auto-linked by label", imageOwner, imageName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if linkedByName, err := tryAutolinkByImageName(ctx, p, imageOwner, imageName, doer); err != nil {
|
||||
return false, err
|
||||
} else if linkedByName {
|
||||
log.Info("Image %s/%s was auto-linked by image name", imageOwner, imageName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Tries to link a package to a repository by label from metadata.
|
||||
// If it fails, it returns false, nil. Only actual errors are returned, so don't use the err return to determine if the linking was performed.
|
||||
func tryAutolinkByLabel(ctx context.Context, p *packages_model.Package, labels map[string]string, doer *user_model.User) (linked bool, err error) {
|
||||
if labels == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
labelRepo, ok := labels["org.opencontainers.image.source"]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(labelRepo)
|
||||
if err != nil {
|
||||
log.Warn("Failed to extract label value org.opencontainers.image.source: value is not in format '{host}/{owner}/{repo}' (is: %s)", labelRepo)
|
||||
return false, nil // we do not return an error here, since a malformed label should simply be ignored
|
||||
}
|
||||
|
||||
fullBasePath := fmt.Sprintf("%s://%s/", u.Scheme, u.Host)
|
||||
if setting.AppURL != fullBasePath {
|
||||
log.Warn("Failed to extract label value org.opencontainers.image.source: host does not match Forgejo AppURL (is: %s, want: %s)", fullBasePath, setting.AppURL)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(pathParts) != 2 {
|
||||
log.Warn("Failed to extract label value org.opencontainers.image.source: value is not in format '{host}/{owner}/{repo}' (is: %s)", labelRepo)
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, pathParts[0], pathParts[1])
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
return false, err // this is a legit error
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := packages_service.LinkToRepository(ctx, p, repository, doer); err != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) {
|
||||
return false, nil // we don't want an error case if the user does not have write access to the repo they have write access to
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Tries to link a package to a repository by its name (using {owner}/{repo}[/...]).
|
||||
// If it fails, it returns false, nil. Only actual errors are returned, so don't use the err return to determine if the linking was performed.
|
||||
func tryAutolinkByImageName(ctx context.Context, p *packages_model.Package, imageOwner, imageName string, doer *user_model.User) (linked bool, err error) {
|
||||
repoName := strings.SplitN(imageName, "/", 2)[0] // [0] = repo; [1] = remainer (no need to check length since SplitN always returns at least one element)
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, imageOwner, repoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
return false, err // this is a legit error
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
if err := packages_service.LinkToRepository(ctx, p, repository, doer); err != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) {
|
||||
return false, nil // we don't want an error case if the user does not have write access to the repo they have write access to
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import (
|
|||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
|
@ -17,12 +19,15 @@ import (
|
|||
"forgejo.org/models/db"
|
||||
packages_model "forgejo.org/models/packages"
|
||||
container_model "forgejo.org/models/packages/container"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
container_module "forgejo.org/modules/packages/container"
|
||||
"forgejo.org/modules/setting"
|
||||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/test"
|
||||
packages_service "forgejo.org/services/packages"
|
||||
"forgejo.org/tests"
|
||||
|
||||
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
|
@ -62,20 +67,20 @@ func TestPackageContainer(t *testing.T) {
|
|||
|
||||
unknownDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
blobDigest := "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
|
||||
blobContent, _ := base64.StdEncoding.DecodeString(`H4sIAAAJbogA/2IYBaNgFIxYAAgAAP//Lq+17wAEAAA=`)
|
||||
blobDigest := "sha256:" + sha256Hash(string(blobContent))
|
||||
|
||||
configDigest := "sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d"
|
||||
configContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}`
|
||||
configDigest := "sha256:" + sha256Hash(configContent)
|
||||
|
||||
manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6"
|
||||
manifestContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
|
||||
manifestDigest := "sha256:" + sha256Hash(manifestContent)
|
||||
|
||||
untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d"
|
||||
untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
|
||||
untaggedManifestDigest := "sha256:" + sha256Hash(untaggedManifestContent)
|
||||
|
||||
indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec"
|
||||
indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}`
|
||||
indexManifestDigest := "sha256:" + sha256Hash(indexManifestContent)
|
||||
|
||||
anonymousToken := ""
|
||||
readUserToken := ""
|
||||
|
|
@ -906,4 +911,208 @@ func TestPackageContainer(t *testing.T) {
|
|||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("AutoLinking", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// create repo which is used for auto-linking
|
||||
repo := createTestRepositoryWithPackageRegistry(t, user, "autolink-repo")
|
||||
|
||||
// Test repo for the private user, used to test unauthorized auto-linking.
|
||||
// We don't need the repo object, but the name is used in the annotation pushed in the test.
|
||||
_ = createTestRepositoryWithPackageRegistry(t, privateUser, "autolink-repo")
|
||||
|
||||
// some paths to push to
|
||||
urlExistingRepo := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, repo.Name)
|
||||
nameNonexistingRepo1 := "nonexisting-repo"
|
||||
urlNonexistingRepo1 := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, nameNonexistingRepo1)
|
||||
nameNonexistingRepo2 := "another-nonexisting-repo"
|
||||
urlNonexistingRepo2 := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, nameNonexistingRepo2)
|
||||
nameNonexistingRepo3 := "secret-repo"
|
||||
urlNonexistingRepo3 := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, nameNonexistingRepo3)
|
||||
nameNonexistingRepo4 := "more-repo-names-generator"
|
||||
urlNonexistingRepo4 := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, nameNonexistingRepo4)
|
||||
nameExistingRepoNested := "nested-image1"
|
||||
urlExistingRepoNested := fmt.Sprintf("%sv2/%s/%s/%s", setting.AppURL, user.Name, repo.Name, nameExistingRepoNested)
|
||||
|
||||
// variable to hold an auto-linked package, which will be unlinked again in a later test
|
||||
var linkedPackage *packages_model.Package
|
||||
|
||||
t.Run("PushToArbitraryRepo", func(t *testing.T) {
|
||||
// Upload blobs and manifest
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo1, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo1, configDigest), strings.NewReader(configContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlNonexistingRepo1, "v1"), strings.NewReader(manifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
p, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, nameNonexistingRepo1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nameNonexistingRepo1, p.Name) // just to make sure we have grabbed the correct package
|
||||
assert.Equal(t, int64(0), p.RepoID)
|
||||
})
|
||||
|
||||
t.Run("PushToExisingRepo", func(t *testing.T) {
|
||||
// Upload blobs and manifest which should create a package with tag "v1"
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlExistingRepo, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlExistingRepo, configDigest), strings.NewReader(configContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlExistingRepo, "v1"), strings.NewReader(manifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// get the resulting package
|
||||
p, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, repo.Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, repo.Name, p.Name) // just to make sure we have grabbed the correct package
|
||||
assert.Equal(t, repo.ID, p.RepoID)
|
||||
linkedPackage = p // store auto-linked package for the next test
|
||||
})
|
||||
|
||||
t.Run("PushToExistingRepoNested", func(t *testing.T) {
|
||||
// Upload blobs and manifest which should create a package with tag "v1"
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlExistingRepoNested, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlExistingRepoNested, configDigest), strings.NewReader(configContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlExistingRepoNested, "v1"), strings.NewReader(manifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// get the resulting package
|
||||
p, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, repo.Name+"/"+nameExistingRepoNested)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, repo.Name+"/"+nameExistingRepoNested, p.Name) // just to make sure we have grabbed the correct package
|
||||
assert.Equal(t, repo.ID, p.RepoID)
|
||||
})
|
||||
|
||||
t.Run("PushVersionToUnlinkedRepo", func(t *testing.T) {
|
||||
// unlink previously auto-linked package
|
||||
require.NoError(t,
|
||||
packages_service.UnlinkFromRepository(t.Context(), linkedPackage, user),
|
||||
)
|
||||
// test if correctly unlinked
|
||||
checkPackageForUnlinked, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, repo.Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), checkPackageForUnlinked.RepoID)
|
||||
|
||||
// push updated version (e.g. tag v2)
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlExistingRepo, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlExistingRepo, configDigest), strings.NewReader(configContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlExistingRepo, "v2"), strings.NewReader(manifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// test if still unlinked
|
||||
checkPackageForStillUnlinked, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, repo.Name)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), checkPackageForStillUnlinked.RepoID)
|
||||
})
|
||||
|
||||
t.Run("PushWithLabel", func(t *testing.T) {
|
||||
// Pushes to non-existing path but tries to link using an image label.
|
||||
|
||||
// same as configContent, but with the added label in config: "org.opencontainers.image.source": "{AppURL}/user2/autolink-repo"
|
||||
configWithOpenContainersSourceLabelContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Labels":{"org.opencontainers.image.source":"` + setting.AppURL + `user2/autolink-repo"},"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}`
|
||||
configWithOpenContainersSourceLabelDigest := "sha256:" + sha256Hash(configWithOpenContainersSourceLabelContent)
|
||||
manifestWithOpenContainersSourceLabelContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"` + configWithOpenContainersSourceLabelDigest + `","size":` + strconv.Itoa(len(configWithOpenContainersSourceLabelContent)) + `},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"` + blobDigest + `","size":32}]}`
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo2, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo2, configWithOpenContainersSourceLabelDigest), strings.NewReader(configWithOpenContainersSourceLabelContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlNonexistingRepo2, "v1"), strings.NewReader(manifestWithOpenContainersSourceLabelContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
p, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, nameNonexistingRepo2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nameNonexistingRepo2, p.Name) // just to make sure we have grabbed the correct package
|
||||
assert.Equal(t, repo.ID, p.RepoID)
|
||||
})
|
||||
|
||||
t.Run("PushWithAnnotation", func(t *testing.T) {
|
||||
// Pushes to non-existing path but tries to link using a push annotation in the manifest.
|
||||
|
||||
// same as configContent, but with the added annotation directly within the manifest: "org.opencontainers.image.source": "{AppURL}/user2/autolink-repo"
|
||||
manifestWithOpenContainersSourceAnnotationContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"` + configDigest + `","size":` + strconv.Itoa(len(configContent)) + `},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"` + blobDigest + `","size":32}],"annotations":{"org.opencontainers.image.source":"` + setting.AppURL + `user2/autolink-repo"}}`
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo3, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo3, configDigest), strings.NewReader(configContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlNonexistingRepo3, "v1"), strings.NewReader(manifestWithOpenContainersSourceAnnotationContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
p, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, nameNonexistingRepo3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nameNonexistingRepo3, p.Name) // just to make sure we have grabbed the correct package
|
||||
assert.Equal(t, repo.ID, p.RepoID)
|
||||
})
|
||||
|
||||
t.Run("PushWithAnnotationNoPermissions", func(t *testing.T) {
|
||||
// This tests pushes a manifest as user2, but tries to link to an existing repo of user31.
|
||||
// This should fail silently with the created package not automatically getting linked.
|
||||
|
||||
// same as configContent above (also uses blob[Digest/Content]), but with an added annotation to auto-link to a repo of the private user: "org.opencontainers.image.source": "{AppURL}/user31/autolink-repo"
|
||||
manifestWithOpenContainersSourceAnnotationPrivateUserContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"` + configDigest + `","size":` + strconv.Itoa(len(configContent)) + `},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"` + blobDigest + `","size":32}],"annotations":{"org.opencontainers.image.source":"` + setting.AppURL + `user31/autolink-repo"}}`
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo4, blobDigest), bytes.NewReader(blobContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", urlNonexistingRepo4, configDigest), strings.NewReader(configContent)).
|
||||
AddTokenAuth(userToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", urlNonexistingRepo4, "v1"), strings.NewReader(manifestWithOpenContainersSourceAnnotationPrivateUserContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
MakeRequest(t, req, http.StatusCreated) // wrongly annotated pushes still get pushed, but not auto linked
|
||||
|
||||
p, err := packages_model.GetPackageByName(t.Context(), user.ID, packages_model.TypeContainer, nameNonexistingRepo4)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, nameNonexistingRepo4, p.Name) // just to make sure we have grabbed the correct package
|
||||
assert.Equal(t, int64(0), p.RepoID) // ensure not linked
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func createTestRepositoryWithPackageRegistry(t *testing.T, user *user_model.User, name string) *repo_model.Repository {
|
||||
ctx := NewAPITestContext(t, user.Name, name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
t.Run("CreateRepo", doAPICreateRepository(ctx, nil, git.Sha1ObjectFormat, func(t *testing.T, r api.Repository) {
|
||||
require.True(t, r.HasPackages)
|
||||
}))
|
||||
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, name)
|
||||
require.NoError(t, err)
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func sha256Hash(in string) string {
|
||||
sum := sha256.Sum256([]byte(in))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue