Skip to content

Commit

Permalink
fix(nexus): set accept headers to prefer docker schema 2 v2 when avai…
Browse files Browse the repository at this point in the history
…lable (#719)

* fix(nexus): request docker manifest schema 2 v2 in headers

* use request assertions to check headers directly

* address feedback

* reindent comment

* Update plugins/nexus-repository-manager/src/api/nexus-repository-manager-api-client/nexus-repository-manager-api-client.ts

Co-authored-by: Paul Schultz <[email protected]>

* Address feedback re: getAdditionalHeaders

---------

Co-authored-by: Paul Schultz <[email protected]>
  • Loading branch information
rmartine-ias and schultzp2020 authored Sep 19, 2023
1 parent e751d70 commit 29d9c89
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 17214,
"digest": "sha256:b98e210da4230ea59323687f2b6678e12f00a8c82a9433f8235a02e50a06c640"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 36581161,
"digest": "sha256:7890eb22610600843a22de84c96fab3f2d428d19e164a529d775ebbb22cc2f3e"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 36267603,
"digest": "sha256:cfdca8bd8795bb3e2c17030868e88434c52d55b7727a10187c9c0b7d0884daf0"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 33954115,
"digest": "sha256:d47076810fbfe12fe7ffed77b6e7e314d8c2edec4d191cbf5e2297feed9bfd80"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32,
"digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 132692565,
"digest": "sha256:b6c0e061017b8d76a988fe0a2e985baec96255966cf7fd71f36eae65c97d6bf4"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 132761418,
"digest": "sha256:f248198ee67af270e04ecac09c0bd34eb06025cd8db9e165146a78a603bc906b"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"schemaVersion": 1,
"name": "janus-idp/backstage-showcase",
"tag": "sha-de3dbf1",
"fsLayers": [
{
"blobSum": "sha256:7890eb22610600843a22de84c96fab3f2d428d19e164a529d775ebbb22cc2f3e"
},
{
"blobSum": "sha256:cfdca8bd8795bb3e2c17030868e88434c52d55b7727a10187c9c0b7d0884daf0"
},
{
"blobSum": "sha256:8b819f6efa3a8cd38f30259971941291a36fa6472a83ae6bf9bc79119d0e4c87"
},
{
"blobSum": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1"
},
{
"blobSum": "sha256:ee443150ae3adcf4f259a86940e6095cd661cad1c31774d198f5d8f85adfd28d"
},
{
"blobSum": "sha256:0fc00d970ac8cb78ca3040c28de02350f68f2eeb4d30c211d94aa06f1b093460"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"items": [
{
"id": "ZG9ja2VyOmRjMDg4NmQ4MTk4MjI1M2ZkYjRlNjFkYTU0NTY4N2Ri",
"repository": "docker",
"format": "docker",
"group": null,
"name": "janus-idp/backstage-showcase",
"version": "sha-33dfe6b",
"assets": [
{
"downloadUrl": "http://localhost:8081/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-33dfe6b",
"path": "v2/janus-idp/backstage-showcase/manifests/sha-33dfe6b",
"id": "ZG9ja2VyOjYyNTRhNjg1YWM5Mzc4N2ZkOTY5ZWMxN2YxZDQzNjZm",
"repository": "docker",
"format": "docker",
"checksum": {
"sha1": "3ae9cd4ea015cc0cd2d254d9f255235256872fd2",
"sha256": "58433b2aa31be8d65bca3eedc83cd49aed997deef39dded5f1c7be1a9227d25f"
},
"contentType": "application/vnd.docker.distribution.manifest.v2+json",
"lastModified": "2023-07-27T21:38:10.664+00:00",
"lastDownloaded": "2023-08-07T17:47:19.273+00:00",
"uploader": "admin",
"uploaderIp": "0.0.0.0",
"fileSize": 1586
}
]
}
],
"continuationToken": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"items": [
{
"id": "ZG9ja2VyOjA5OTViYmFlZWIxZmJjM2VjYzEzODJkMjkwMzM2NzA1",
"repository": "docker",
"format": "docker",
"group": null,
"name": "janus-idp/backstage-showcase",
"version": "sha-de3dbf1",
"assets": [
{
"downloadUrl": "http://localhost:8081/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-de3dbf1",
"path": "v2/janus-idp/backstage-showcase/manifests/sha-de3dbf1",
"id": "ZG9ja2VyOjE0NDUzNzRhZDc5YTczODFmOTIzY2I5OWUzYjJmYTBk",
"repository": "docker",
"format": "docker",
"checksum": {
"sha1": "206f5cfc76a16dbba78e2b9a826cbe7bd5cdd7dd",
"sha256": "85aa455189b4dba87108d57ec3b223b1766e15c16cb03b046a17bdfcddb37cc3"
},
"contentType": "application/vnd.docker.distribution.manifest.v2+json",
"lastModified": "2023-07-27T21:38:03.120+00:00",
"lastDownloaded": "2023-08-07T17:47:19.273+00:00",
"uploader": "admin",
"uploaderIp": "0.0.0.0",
"fileSize": 1586
}
]
}
],
"continuationToken": null
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,41 @@ const handlers = [
},
),

// Always returns manifest v2 schema 1, regardless of the accept header, to
// simulate a server that does not support schema 2.
rest.get(
`${LOCAL_ADDR}/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-33dfe6b`,
(_, res, ctx) => {
return res(
ctx.status(200),
ctx.json(
require(
`${__dirname}/../../__fixtures__/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-33dfe6b.json`,
`${__dirname}/../../__fixtures__/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-33dfe6b-schema1.json`,
),
),
);
},
),

// Conditionally returns manifest v2 schema 1/2, depending on the accept
// header, to simulate a server that supports both schemas.
rest.get(
`${LOCAL_ADDR}/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-de3dbf1`,
(_, res, ctx) => {
(req, res, ctx) => {
let fixtureName = 'sha-de3dbf1-schema1.json';
if (
req.headers
.get('accept')
?.includes('application/vnd.docker.distribution.manifest.v2+json')
) {
fixtureName = 'sha-de3dbf1-schema2.json';
}

return res(
ctx.status(200),
ctx.json(
require(
`${__dirname}/../../__fixtures__/repository/docker/v2/janus-idp/backstage-showcase/manifests/sha-de3dbf1.json`,
`${__dirname}/../../__fixtures__/repository/docker/v2/janus-idp/backstage-showcase/manifests/${fixtureName}`,
),
),
);
Expand Down Expand Up @@ -116,6 +129,10 @@ describe('NexusRepositoryManagerApiClient', () => {
});
});

afterEach(() => {
server.events.removeAllListeners();
});

it('should use the default proxy path', async () => {
const { components } = await nexusApi.getComponents({
dockerImageName: 'janus-idp/backstage-showcase',
Expand Down Expand Up @@ -145,6 +162,39 @@ describe('NexusRepositoryManagerApiClient', () => {
);
});

it('sets headers requesting manifest 2 schema 2', async () => {
server.events.on('request:start', req => {
let expectedAcceptHeader = 'application/json';
if (req.url.pathname.includes('/manifests')) {
expectedAcceptHeader =
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.v1+json;q=0.9, */*;q=0.8';
}

expect(req.headers.get('accept')).toEqual(expectedAcceptHeader);
});

const { components } = await nexusApi.getComponents({
dockerImageTag: 'sha-de3dbf1',
});

expect(components[0]?.rawAssets[0]?.schemaVersion).toEqual(2);
});

it('should not set special headers for non-docker GETs', async () => {
server.events.on('request:start', req => {
expect(req.headers.get('accept')).not.toContain(
'application/vnd.docker',
);
});

// Catching 404 is temporary until there are fixtures for maven.
await expect(
nexusApi.getComponents({
mavenGroupId: 'com.example',
}),
).rejects.toThrow('Not Found');
});

it('should return components using dockerImageTag', async () => {
const { components } = await nexusApi.getComponents({
dockerImageTag: 'latest',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ const NEXUS_REPOSITORY_MANAGER_CONFIG = {
experimentalAnnotations: 'nexusRepositoryManager.experimentalAnnotations',
} as const;

/**
* Indicates that we want manifest v2 schema 2 if possible. It's faster
* for supporting servers to return and contains size information.
* @see @link{https://docs.docker.com/registry/spec/manifest-v2-2/#backward-compatibility|Backward compatibility}
*/
const DOCKER_MANIFEST_HEADERS = {
Accept: [
'application/vnd.docker.distribution.manifest.v2+json',
'application/vnd.docker.distribution.manifest.v1+json;q=0.9',
'*/*;q=0.8',
].join(', '),
} as const satisfies HeadersInit;

function getAdditionalHeaders(format?: string): HeadersInit {
switch (format /* NOSONAR - use switch for expandability */) {
case 'docker':
return DOCKER_MANIFEST_HEADERS;
default:
return {};
}
}

export type NexusRepositoryManagerApiV1 = {
getComponents(query: SearchServiceQuery): Promise<{
components: {
Expand Down Expand Up @@ -74,14 +96,19 @@ export class NexusRepositoryManagerApiClient
return await SearchService.search(query);
}

private async fetcher(url: string) {
private async fetcher(url: string, additionalHeaders: HeadersInit = {}) {
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});

const headers = new Headers(additionalHeaders);
if (!headers.has('Accept')) {
headers.set('Accept', 'application/json');
}

if (idToken) {
headers.set('Authorization', `Bearer ${idToken}`);
}

const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(
`failed to fetch data, status ${response.status}: ${response.statusText}`,
Expand All @@ -90,7 +117,7 @@ export class NexusRepositoryManagerApiClient
return await response.json();
}

private async getRawAsset(url?: string) {
private async getRawAsset(url?: string, additionalHeaders: HeadersInit = {}) {
const proxyUrl = await this.getBaseUrl();

if (!url) {
Expand All @@ -99,13 +126,19 @@ export class NexusRepositoryManagerApiClient

const path = /\/(repository\/.*)/.exec(url)?.at(1);

return (await this.fetcher(`${proxyUrl}/${path}`)) as RawAsset;
return (await this.fetcher(
`${proxyUrl}/${path}`,
additionalHeaders,
)) as RawAsset;
}

private async getRawAssets(component: ComponentXO) {
const additionalHeaders = getAdditionalHeaders(component.format);

const assets = await Promise.all(
component.assets?.map(
async asset => await this.getRawAsset(asset.downloadUrl),
async asset =>
await this.getRawAsset(asset.downloadUrl, additionalHeaders),
// Create a dummy promise to avoid Promise.all() from failing
) ?? [new Promise<null>(() => null)],
);
Expand Down

0 comments on commit 29d9c89

Please sign in to comment.