Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add referrersHandler for artifact manifest #34

Merged
merged 4 commits into from
Nov 25, 2022
Merged

feat!: add referrersHandler for artifact manifest #34

merged 4 commits into from
Nov 25, 2022

Conversation

wangxiaoxuan273
Copy link

@wangxiaoxuan273 wangxiaoxuan273 commented Nov 10, 2022

This pr implements referrersHandler and its related operations. For this pr only artifact manifest referrers are supported. Support for image manifest referrers will be added in a future pr. The implementation is based on the existing rc2 branch, which implements referrersHandler as an extension. Changes the registry/handler package.

Part 7 of #21.

@wangxiaoxuan273
Copy link
Author

wangxiaoxuan273 commented Nov 10, 2022

I have run a basic test.

  • Setting: subject.json is a minimal artifact manifest. ref01.json, ref02.json, ref03.json are artifact manifests that refer to subject.json.
  • Test 1: Get the referrers of subject.json. An image index with a manifest list of length 3 should be returned.
  • Test 2: Get the referrers of ref03.json, which has no referrers. An image index with an empty manifest list should be returned.

Test result:

  • Test 1:

image

image

Prettified returned result:

{
	"schemaVersion": 2,
	"mediaType": "application/vnd.oci.image.index.v1+json",
	"manifests": [{
		"mediaType": "application/vnd.oci.artifact.manifest.v1+json",
		"digest": "sha256:6696649520b4354ecb926244ea2cebe3fa5ad4f109c1f15f317b50da75b3f92c",
		"size": 392,
		"annotations": {
			"tick": "tock",
			"woo": "doo"
		},
		"artifactType": "application/referrer2"
	}, {
		"mediaType": "application/vnd.oci.artifact.manifest.v1+json",
		"digest": "sha256:b971dd46783d9f43a1aba9018cfa10125a23e5fdb47fab252f6919f85439d811",
		"size": 325,
		"artifactType": "application/referrerexample"
	}, {
		"mediaType": "application/vnd.oci.artifact.manifest.v1+json",
		"digest": "sha256:cd07ace460801b6441c6aa1d9a738cc37514da66553f4257048ad7f59948535b",
		"size": 406,
		"annotations": {
			"bar": "bbaarr",
			"foo": "ffoooo"
		},
		"artifactType": "application/vnd.example.sbom.v1"
	}]
}

Test 2:
image

@codecov-commenter
Copy link

codecov-commenter commented Nov 10, 2022

Codecov Report

Merging #34 (96ff32f) into main (ad70be1) will decrease coverage by 0.68%.
The diff coverage is 1.47%.

@@            Coverage Diff             @@
##             main      #34      +/-   ##
==========================================
- Coverage   57.16%   56.48%   -0.69%     
==========================================
  Files         107      108       +1     
  Lines       11041    11176     +135     
==========================================
+ Hits         6312     6313       +1     
- Misses       4019     4153     +134     
  Partials      710      710              
Impacted Files Coverage Δ
registry/handlers/manifests.go 53.18% <ø> (ø)
registry/handlers/referrers.go 0.00% <0.00%> (ø)
registry/storage/paths.go 69.31% <25.00%> (-1.12%) ⬇️
registry/handlers/app.go 47.08% <100.00%> (+0.07%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

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

const createAnnotationName = "io.cncf.oras.artifact.created"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be OCI version?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to "org.opencontainers.artifact.created".

return err
}

artifactManifest, ok := man.(*ociartifact.DeserializedManifest)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to cover OCI image manifest referrers in this PR?

Copy link
Author

@wangxiaoxuan273 wangxiaoxuan273 Nov 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would do that in a separate pr. For this pr we only covers OCI artifact manifest. Thanks for pointing this out.

}

w.Header().Set("Content-Type", v1.MediaTypeImageIndex)
w.Header().Set("ORAS-Api-Version", "oras/1.0")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still relevant?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed it.

}

func generateLinkHeader(repoName, subjectDigest, artifactType string, lastDigests []string, nPage int) string {
url := fmt.Sprintf("/v2/%s/_oras/artifacts/referrers?digest=%s&nextToken=%s",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The url needs to be updated

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated this url.

@shizhMSFT
Copy link

@wangxiaoxuan273 Could you split this PR into two PRs? One for indexing the newly pushed OCI Artifact manifest. One for query the referrers API?

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

const createAnnotationName = "org.opencontainers.artifact.created"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shizhMSFT Is this variable okay? I didn't find any related info about this in the spec.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try searching org.opencontainers.artifact.created in https://github.com/opencontainers/distribution-spec/blob/main/spec.md

Copy link

@shizhMSFT shizhMSFT Nov 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: createAnnotationName for Artifact manifest and image manifest are different.

Copy link
Author

@wangxiaoxuan273 wangxiaoxuan273 Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found relevant information in this page. Added doc comments in the code.

}

func generateLinkHeader(repoName, subjectDigest, artifactType string, lastDigests []string, nPage int) string {
url := fmt.Sprintf("/v2/%s/_referrers/manifests/referrers?digest=%s&nextToken=%s",
Copy link
Author

@wangxiaoxuan273 wangxiaoxuan273 Nov 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shizhMSFT Is this url okay?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL is defined in https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers

There is no such a field called nextToken.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Therefore, nextToken is an implementation specific field. You might want to document this somewhere.

Copy link
Author

@wangxiaoxuan273 wangxiaoxuan273 Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may not need the nextToken field. I've created an issue to keep track of the paging feature of referrers, and I'll look deeper into it when fixing that issue.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the url according to the distribution spec.

@wangxiaoxuan273 wangxiaoxuan273 changed the title feat!: add referrersHandler feat!: add referrersHandler that works for artifact manifest Nov 21, 2022
Comment on lines 21 to 24
var (
rootPrefix = []string{storagePathRoot, storagePathVersion}
repoPrefix = append(rootPrefix, "repositories")
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to put it back as local variables.

Think about the following code.

storagePathVersion := "v2"
storagePathRoot := "/docker/registry/"
rootPrefix := []string{storagePathRoot, storagePathVersion}
repoPrefix := append(rootPrefix, "repositories")

pathA := append(repoPrefix, "foo")
pathB := append(repoPrefix, "bar")
fmt.Println(pathA)
fmt.Println(pathB)

The output is

[/docker/registry/ v2 repositories bar]
[/docker/registry/ v2 repositories bar]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put it back.

Comment on lines 27 to 34
// Reference for pre-defined annotation keys:
// https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
const createAnnotationNameArtifactManifest = "org.opencontainers.artifact.created"
const createAnnotationTimestampFormat = time.RFC3339
const maxPageSize = 100

// minimum page size used for # of digests to put in nextToken
const minPageSize = 3

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to make them const blocks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed.

Comment on lines 86 to 87
artifactType := r.FormValue("artifactType")
nPage, nParseError := strconv.Atoi(r.FormValue("n"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do FormValue apply to GET requests?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced FormValue with r.URL.Query()

}
}

func (h *referrersHandler) generateSortedReferrers(ctx context.Context, revision digest.Digest, artifactType string) ([]v1.Descriptor, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does OCI Referrer Spec specifies sorting? If so, please point out the spec reference.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed sorting

@wangxiaoxuan273 wangxiaoxuan273 requested review from shizhMSFT and m5i-work and removed request for shizhMSFT November 22, 2022 05:00
func referrersDispatcher(ctx *Context, r *http.Request) http.Handler {
dgst, err := getDigest(ctx)
if err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra blank line?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

}

// ReferrersAPIResponse describes the response body of the referrers API.
type ReferrersAPIResponse struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we are still missing annotations field?

Btw, can we reuse v1.Index?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that v1.Index looks better.

Copy link
Author

@wangxiaoxuan273 wangxiaoxuan273 Nov 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I forgot annotations. Reused v1.Index here instead and added annotations.

}

nextTokens := query["nextToken"]
TokenSet := make(map[string]string)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this var start with capital letter?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is from the original rc2 branch implementation. Removed it now as it is for the paging feature, and paging will be implemented in a separate pr.

}

func generateLinkHeader(repoName, subjectDigest, artifactType string, lastDigests []string, nPage int) string {
url := fmt.Sprintf("/v2/%s/referrers/%s?nextToken=%s",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url.Values() may help here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this function as it is for paging and paging will be implemented in another pr.

dcontext.GetLogger(h).Debug("GetReferrers")

query := r.URL.Query()
n, nExists := query["n"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use Values.Get() method to avoid array boundary issues.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with Get().

}

// ReferrersAPIResponse describes the response body of the referrers API.
type ReferrersAPIResponse struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that v1.Index looks better.

}

nextTokens := query["nextToken"]
TokenSet := make(map[string]string)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it is a set, value is never used.

Suggested change
TokenSet := make(map[string]string)
tokenSet := make(map[string]struct{})

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this part as it is part of the paging feature.

Comment on lines 251 to 264
func enumerateReferrerLinks(ctx context.Context,
rootPath string,
stDriver driver.StorageDriver,
repository distribution.Repository,
blobstatter distribution.BlobStatter,
subjectRevision digest.Digest,
artifactManifestIndex map[digest.Digest][]digest.Digest,
ingestor func(ctx context.Context,
digest digest.Digest,
subjectRevision digest.Digest,
artifactManifestIndex map[digest.Digest][]digest.Digest,
repository distribution.Repository,
blobstatter distribution.BlobStatter,
storageDriver driver.StorageDriver) error) error {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function declaration does not seem right.

It can be simplified as

Suggested change
func enumerateReferrerLinks(ctx context.Context,
rootPath string,
stDriver driver.StorageDriver,
repository distribution.Repository,
blobstatter distribution.BlobStatter,
subjectRevision digest.Digest,
artifactManifestIndex map[digest.Digest][]digest.Digest,
ingestor func(ctx context.Context,
digest digest.Digest,
subjectRevision digest.Digest,
artifactManifestIndex map[digest.Digest][]digest.Digest,
repository distribution.Repository,
blobstatter distribution.BlobStatter,
storageDriver driver.StorageDriver) error) error {
func enumerateReferrerLinks(ctx context.Context,
rootPath string,
stDriver driver.StorageDriver,
blobstatter distribution.BlobStatter,
ingestor func(digest digest.Digest) error) error {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed accordingly. Thanks for the work.

Comment on lines 295 to 306
err = ingestor(ctx,
digest,
subjectRevision,
artifactManifestIndex,
repository,
blobstatter,
stDriver)
if err != nil {
return err
}

return nil

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you see the pattern

err = do()
if err != nil {
    return err
}
return nil

it can be simplified as

return do()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed accordingly.

Comment on lines 128 to 165
// only consider pagination if # of referrers is greater than page size
if len(referrers) > nPage {
startIndex := 0
if len(TokenSet) > 0 {
for i, ref := range referrers {
// check if ref matches a digest in nextToken list
if _, ok := TokenSet[ref.Digest.String()]; ok {
// set the starting index to the largest index
if (i + 1) > startIndex {
startIndex = i + 1
}
}
}
}

// only applicable if last item provided as nextToken
if startIndex == len(referrers) {
referrers = []v1.Descriptor{}
}

// if there's only 1 page of results left
if len(referrers)-startIndex <= nPage {
referrers = referrers[startIndex:]
} else {
referrers = referrers[startIndex:(startIndex + nPage)]
// nextToken is a base64 encoded comma-separated string of the digests of the last three referrers in the response
var nextDgsts []string
for i := nPage - 1; i >= nPage-minPageSize; i-- {
nextDgsts = append(nextDgsts, referrers[i].Digest.String())
}
// if n was not provided in page, set nPage to a value so link header knows not to include n
if nParseError != nil {
nPage = -1
}
// add the Link Header
w.Header().Set("Link", generateLinkHeader(h.Repository.Named().Name(), h.Digest.String(), filterArtifactType, nextDgsts, nPage))
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next token implementation is strange to me. Could you explain why you choose this algorithm?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the paging part, will do it in a future pr.

Signed-off-by: wangxiaoxuan273 <[email protected]>
Signed-off-by: wangxiaoxuan273 <[email protected]>
Signed-off-by: wangxiaoxuan273 <[email protected]>
@wangxiaoxuan273 wangxiaoxuan273 changed the title feat!: add referrersHandler that works for artifact manifest feat!: add referrersHandler for artifact manifest Nov 25, 2022
Comment on lines 190 to 195
linked, err := digest.Parse(string(content))
if err != nil {
return "", err
}

return linked, nil

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code pattern can be applied.

Suggested change
linked, err := digest.Parse(string(content))
if err != nil {
return "", err
}
return linked, nil
return digest.Parse(string(content))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed accordingly.

Comment on lines 61 to 66
annotations := map[string]string{}
query := r.URL.Query()
artifactTypeFilter := query.Get("artifactType")
if artifactTypeFilter != "" {
annotations[v1.AnnotationReferrersFiltersApplied] = "artifactType"
}
Copy link

@shizhMSFT shizhMSFT Nov 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that nil map with omitempty will be omitted. However, empty map will be encoded to {}.

Here, if artifactType is not there, we should use nil map.

Consider this code pattern

Suggested change
annotations := map[string]string{}
query := r.URL.Query()
artifactTypeFilter := query.Get("artifactType")
if artifactTypeFilter != "" {
annotations[v1.AnnotationReferrersFiltersApplied] = "artifactType"
}
var annotations map[string]string
artifactTypeFilter := r.URL.Query().Get("artifactType")
if artifactTypeFilter != "" {
annotations = map[string]string{
v1.AnnotationReferrersFiltersApplied: "artifactType",
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed accordingly.


func (h *referrersHandler) generateReferrersList(ctx context.Context, subjectDigest digest.Digest, artifactType string) ([]v1.Descriptor, error) {
dcontext.GetLogger(ctx).Debug("(*referrersHandler).generateReferrersList")
var referrers []v1.Descriptor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: code style: let's only declare variables when needed.
suggestion: move this line after line 99 but before line 102

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved accordingly.

Signed-off-by: wangxiaoxuan273 <[email protected]>
Copy link

@shizhMSFT shizhMSFT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants