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(maven): Unify fetching utilities #32999

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
62 changes: 33 additions & 29 deletions lib/modules/datasource/maven/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Readable } from 'node:stream';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { sdkStreamMixin } from '@smithy/util-stream';
import { mockClient } from 'aws-sdk-client-mock';
import { GoogleAuth as _googleAuth } from 'google-auth-library';
import { DateTime } from 'luxon';
Expand Down Expand Up @@ -622,10 +624,7 @@ describe('modules/datasource/maven/index', () => {

describe('post-fetch release validation', () => {
it('returns null for 404', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(404);
httpMock.scope(MAVEN_REPO).get('/foo/bar/1.2.3/bar-1.2.3.pom').reply(404);

const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
Expand All @@ -635,25 +634,23 @@ describe('modules/datasource/maven/index', () => {
expect(res).toBeNull();
});

it('returns null for unknown error', async () => {
it('returns original value for unknown error', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.get('/foo/bar/1.2.3/bar-1.2.3.pom')
.replyWithError('unknown error');

const releaseOrig: Release = { version: '1.2.3' };
const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: MAVEN_REPO },
{ version: '1.2.3' },
releaseOrig,
);

expect(res).toBeNull();
expect(res).toBe(releaseOrig);
});

it('returns original value for 200 response', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(200);
httpMock.scope(MAVEN_REPO).get('/foo/bar/1.2.3/bar-1.2.3.pom').reply(200);
const releaseOrig: Release = { version: '1.2.3' };

const res = await postprocessRelease(
Expand All @@ -665,10 +662,7 @@ describe('modules/datasource/maven/index', () => {
});

it('returns original value for 200 response with versionOrig', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(200);
httpMock.scope(MAVEN_REPO).get('/foo/bar/1.2.3/bar-1.2.3.pom').reply(200);
const releaseOrig: Release = { version: '1.2', versionOrig: '1.2.3' };

const res = await postprocessRelease(
Expand All @@ -683,13 +677,13 @@ describe('modules/datasource/maven/index', () => {
const releaseOrig: Release = { version: '1.2.3' };
expect(
await postprocessRelease(
{ datasource, registryUrl: MAVEN_REPO },
{ datasource, registryUrl: MAVEN_REPO }, // packageName is missing
releaseOrig,
),
).toBe(releaseOrig);
expect(
await postprocessRelease(
{ datasource, packageName: 'foo:bar' },
{ datasource, packageName: 'foo:bar' }, // registryUrl is missing
releaseOrig,
),
).toBe(releaseOrig);
Expand All @@ -698,7 +692,7 @@ describe('modules/datasource/maven/index', () => {
it('adds releaseTimestamp', async () => {
httpMock
.scope(MAVEN_REPO)
.head('/foo/bar/1.2.3/bar-1.2.3.pom')
.get('/foo/bar/1.2.3/bar-1.2.3.pom')
.reply(200, '', { 'Last-Modified': '2024-01-01T00:00:00.000Z' });

const res = await postprocessRelease(
Expand All @@ -719,13 +713,22 @@ describe('modules/datasource/maven/index', () => {
s3mock.reset();
});

function body(input: string) {
const result = new Readable();
zharinov marked this conversation as resolved.
Show resolved Hide resolved
result.push(input);
result.push(null);
return sdkStreamMixin(result);
}

it('checks package', async () => {
s3mock
.on(HeadObjectCommand, {
.on(GetObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.resolvesOnce({});
.resolvesOnce({
Body: body('foo'),
});

const res = await postprocessRelease(
{ datasource, packageName: 'foo:bar', registryUrl: 's3://bucket' },
Expand All @@ -737,11 +740,12 @@ describe('modules/datasource/maven/index', () => {

it('supports timestamp', async () => {
s3mock
.on(HeadObjectCommand, {
.on(GetObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
.resolvesOnce({
Body: body('foo'),
LastModified: DateTime.fromISO(
'2024-01-01T00:00:00.000Z',
).toJSDate(),
Expand All @@ -760,7 +764,7 @@ describe('modules/datasource/maven/index', () => {

it('returns null for deleted object', async () => {
s3mock
.on(HeadObjectCommand, {
.on(GetObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
Expand All @@ -778,7 +782,7 @@ describe('modules/datasource/maven/index', () => {

it('returns null for NotFound response', async () => {
s3mock
.on(HeadObjectCommand, {
.on(GetObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
Expand All @@ -796,7 +800,7 @@ describe('modules/datasource/maven/index', () => {

it('returns null for NoSuchKey response', async () => {
s3mock
.on(HeadObjectCommand, {
.on(GetObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
Expand All @@ -812,9 +816,9 @@ describe('modules/datasource/maven/index', () => {
expect(res).toBeNull();
});

it('returns null for unknown error', async () => {
it('returns original value for any other error', async () => {
s3mock
.on(HeadObjectCommand, {
.on(GetObjectCommand, {
Bucket: 'bucket',
Key: 'foo/bar/1.2.3/bar-1.2.3.pom',
})
Expand All @@ -827,7 +831,7 @@ describe('modules/datasource/maven/index', () => {
releaseOrig,
);

expect(res).toBeNull();
expect(res).toBe(releaseOrig);
});
});
});
Expand Down
76 changes: 43 additions & 33 deletions lib/modules/datasource/maven/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import is from '@sindresorhus/is';
import type { XmlDocument } from 'xmldoc';
import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger';
import * as packageCache from '../../../util/cache/package';
import { cache } from '../../../util/cache/package/decorator';
import { Result } from '../../../util/result';
import { ensureTrailingSlash } from '../../../util/url';
import mavenVersion from '../../versioning/maven';
import * as mavenVersioning from '../../versioning/maven';
Expand All @@ -18,10 +18,10 @@ import type {
ReleaseResult,
} from '../types';
import { MAVEN_REPO } from './common';
import type { MavenDependency } from './types';
import type { MavenDependency, MavenFetchError } from './types';
import {
checkResource,
createUrlForDependencyPom,
downloadMaven,
downloadMavenXml,
getDependencyInfo,
getDependencyParts,
Expand Down Expand Up @@ -93,24 +93,29 @@ export class MavenDatasource extends Datasource {
return cachedVersions;
}

const { isCacheable, xml: mavenMetadata } = await downloadMavenXml(
this.http,
metadataUrl,
);
if (!mavenMetadata) {
return [];
}

const versions = extractVersions(mavenMetadata);
const cachePrivatePackages = GlobalConfig.get(
'cachePrivatePackages',
false,
);
if (cachePrivatePackages || isCacheable) {
await packageCache.set(cacheNamespace, cacheKey, versions, 30);
}

return versions;
const metadataXmlResult = await downloadMavenXml(this.http, metadataUrl);
return metadataXmlResult
.transform(
async ({ isCacheable, data: mavenMetadata }): Promise<string[]> => {
const versions = extractVersions(mavenMetadata);
const cachePrivatePackages = GlobalConfig.get(
'cachePrivatePackages',
false,
);

if (cachePrivatePackages || isCacheable) {
await packageCache.set(cacheNamespace, cacheKey, versions, 30);
}

return versions;
},
)
.onError((err) => {
logger.debug(
`Maven: error fetching versions for "${dependency.display}": ${err.type}`,
);
})
.unwrapOr([]);
}

async getReleases({
Expand Down Expand Up @@ -190,17 +195,22 @@ export class MavenDatasource extends Datasource {
);

const artifactUrl = getMavenUrl(dependency, registryUrl, pomUrl);

const res = await checkResource(this.http, artifactUrl);

if (res === 'not-found' || res === 'error') {
return 'reject';
}

if (is.date(res)) {
release.releaseTimestamp = res.toISOString();
}

return release;
const fetchResult = await downloadMaven(this.http, artifactUrl);
return fetchResult
.transform((res): PostprocessReleaseResult => {
if (res.lastModified) {
release.releaseTimestamp = res.lastModified;
}

return release;
})
.catch((err): Result<PostprocessReleaseResult, MavenFetchError> => {
if (err.type === 'not-found') {
return Result.ok('reject');
}

return Result.ok(release);
})
.unwrapOr(release);
}
}
9 changes: 1 addition & 8 deletions lib/modules/datasource/maven/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { XmlDocument } from 'xmldoc';
import type { Result } from '../../../util/result';
import type { ReleaseResult } from '../types';

Expand All @@ -9,13 +8,6 @@ export interface MavenDependency {
dependencyUrl: string;
}

export interface MavenXml {
isCacheable?: boolean;
xml?: XmlDocument;
}

export type HttpResourceCheckResult = 'found' | 'not-found' | 'error' | Date;

export type DependencyInfo = Pick<
ReleaseResult,
'homepage' | 'sourceUrl' | 'packageScope'
Expand All @@ -41,6 +33,7 @@ export type MavenFetchError =
| { type: 'unsupported-protocol' }
| { type: 'credentials-error' }
| { type: 'missing-aws-region' }
| { type: 'xml-parse-error'; err: Error }
| { type: 'unknown'; err: Error };

export type MavenFetchResult<T = string> = Result<
Expand Down
38 changes: 22 additions & 16 deletions lib/modules/datasource/maven/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { HOST_DISABLED } from '../../../constants/error-messages';
import { Http, HttpError } from '../../../util/http';
import type { MavenFetchError } from './types';
import {
checkResource,
downloadHttpProtocol,
downloadMavenXml,
downloadS3Protocol,
Expand Down Expand Up @@ -46,17 +45,36 @@ function httpError({

describe('modules/datasource/maven/util', () => {
describe('downloadMavenXml', () => {
it('returns empty object for unsupported protocols', async () => {
it('returns error for unsupported protocols', async () => {
const res = await downloadMavenXml(
http,
new URL('unsupported://server.com/'),
);
expect(res).toEqual({});
expect(res.unwrap()).toEqual({
ok: false,
err: { type: 'unsupported-protocol' } satisfies MavenFetchError,
});
});

it('returns error for xml parse error', async () => {
const http = partial<Http>({
get: () =>
Promise.resolve({
statusCode: 200,
body: 'invalid xml',
headers: {},
}),
});
const res = await downloadMavenXml(http, 'https://example.com/');
expect(res.unwrap()).toEqual({
ok: false,
err: { type: 'xml-parse-error', err: expect.any(Error) },
});
});
});

describe('downloadS3Protocol', () => {
it('fails for non-S3 URLs', async () => {
it('returns error for non-S3 URLs', async () => {
const res = await downloadS3Protocol(new URL('http://not-s3.com/'));
expect(res.unwrap()).toEqual({
ok: false,
Expand Down Expand Up @@ -122,16 +140,4 @@ describe('modules/datasource/maven/util', () => {
});
});
});

describe('checkResource', () => {
it('returns not found for unsupported protocols', async () => {
const res = await checkResource(http, 'unsupported://server.com/');
expect(res).toBe('not-found');
});

it('returns error for invalid URLs', async () => {
const res = await checkResource(http, 'not-a-valid-url');
expect(res).toBe('error');
});
});
});
Loading
Loading