Dependency Proxy for packages: metadata endpoints
🔥 Problem
The dependency proxy for packages not only needs to deal with package files but also metadata responses.
🤔 What are metadata responses?
Package manager clients will usually rely on dedicated endpoints to know the state of the package registry. Usually, it's to know things like:
- what are the packages available.
- given a package, what are the versions available.
- where are the package files located.
Depending on the package format, these endpoints can work at different level: registry wide, package name level or package name + version level. Most of the time, it is up to the registry to generate the correct response for these endpoints.
Here is a summary of all GA formats:
| Format | Metadata level | Metadata format | Notes |
|---|---|---|---|
| Maven | xml | package | Content managed by clients. |
| NPM | json | package | Content managed by registry. |
| NuGet | json | package + registry | Content managed by registry. |
| PyPI | html | package version + package + registry | Content managed by registry. |
| Generic | - | - | - |
😱 Dependency proxyfying metadata responses
In the context of dependency proxies, these endpoints become challenging because:
- The response can contain urls or references to the registry itself. This is a problem for the dependency proxy as we want to return response in such a way that clients will always go through the GitLab instance for all interactions.
- The main problem is that the contents of metadata response are more or less cached by clients. They can even use these contents to build the lock file (this is the file that will exactly list the packages (name+version) of a given application).
- Take a CI job that build an application for example here. It will read the lock file to pull all the actual files. If the lock file references the upstream, the dependency proxy is completely avoided which is not what we want.
(2.) might be hard to understand. Let's imagine an example for package format foobar. Let's say that the registry has to implement a metadata endpoint packages/<package_name>/<package_version> that need to return the url of the package archive. Let's say that the official registry is located at https://registy.foobar.org, accessing https://registy.foobar.org/packages/test/1.3.7 might return https://registy.foobar.org/files/test-1.3.7.zip. All good here.
Now, let's say that we have a GitLab dependency proxy for foobar and we point to https://registy.foobar.org. Accessing https://gitlab.com/api/v4/projects/123/dependency_proxy/packages/foobar/packages/test/1.3.7 in the GitLab dependency proxy should return an url. If we simply take the response from the upstream and return it, the clients will still access https://registy.foobar.org and that's not what we really want. Instead, the answer of that endpoint should be https://gitlab.com/api/v4/projects/123/dependency_proxy/packages/foobar/....
In simple words, we have a client that asks for a metadata response and we need to do:
- Pull that response from upstream.
- Modify it to replace all references to upstream by references to the dependency proxy.
- Return it back to the client while uploading it to the dependency proxy (caching). This part seems hard but it's already handled by workhorse.
The entire question is thus: how can we implement (1.) and (2.) in an efficient manner. Keep in mind that metadata response can be very large documents. Again some package formats will have a metadata response at the registry level. This means that the document will describe every package that is available in the registry. You can imagine that for public registries, this response is pretty large.
🗜️ Restrictions
How the Dependency Proxy feature works brings us some restrictions:
- The big one: we can't use background jobs.
- When the dependency proxy needs to do this, there is a client waiting for the file. We can't serve the file from the upstream and have a background job modify it because that means that the first request receives a version and subsequent requests (after the background job ends) get a different version.
- The modification to do on files should be mainly replacing urls but it could be as complex as rewriting structure (like rewriting json structures).
- From the GA formats above, the formats we need to deal with are:
jsonandhtml.- Bonus points of we can deal with the
xmlformat. Future package formats (such asrpm) havexmlmetadata responses.
- Bonus points of we can deal with the
- Metadata responses can be large. Examples: metadata for
npmitself is22+ MB. The registry level metadata tends to be very, very large because it describes the entire registry state (all packages with all available versions). - This needs to be fast as we have a web request waiting for an answer.
- This file is not fetched in one step and returned to the client in an other step. We leverage the workhorse
dependencyproxylogic, where the file is streamed from the upstream back to the client.- This is to say that we have
rubyandgolangat our disposal.
- This is to say that we have
The good news: we are going to cache this result so that next time the same metadata is requested, we can serve the cache entry.