mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
c6578a476b
* support AWS S3 as a chart repository * support creds via hostRules * apply suggestions * fix(datasource/helm): classify s3 lookup errors * test: use await expect().resolves
460 lines
14 KiB
TypeScript
460 lines
14 KiB
TypeScript
import { Readable } from 'node:stream';
|
|
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
import { mockClient } from 'aws-sdk-client-mock';
|
|
import { codeBlock } from 'common-tags';
|
|
import { Fixtures } from '~test/fixtures.ts';
|
|
import * as httpMock from '~test/http-mock.ts';
|
|
import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages.ts';
|
|
import * as hostRules from '../../../util/host-rules.ts';
|
|
import { getPkgReleases } from '../index.ts';
|
|
import { HelmDatasource } from './index.ts';
|
|
|
|
// Truncated index.yaml file
|
|
const indexYaml = Fixtures.get('index.yaml');
|
|
|
|
describe('modules/datasource/helm/index', () => {
|
|
describe('getReleases', () => {
|
|
it('returns null if packageName was not provided', async () => {
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: undefined as never, // #22198
|
|
registryUrls: ['https://example-repository.com'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null if repository was not provided', async () => {
|
|
// FIXME: should it call default rtegisty?
|
|
httpMock
|
|
.scope('https://charts.helm.sh')
|
|
.get('/stable/index.yaml')
|
|
.reply(404);
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'some_chart',
|
|
registryUrls: [],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null for empty response', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200);
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'non_existent_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null for missing response body', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200);
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'non_existent_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null for 404', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(404);
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'some_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('throws for 5xx', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(502);
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'some_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('returns null for unknown error', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.replyWithError('');
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'some_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null if index.yaml in response is empty', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200, '# A comment');
|
|
const releases = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'non_existent_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
});
|
|
expect(releases).toBeNull();
|
|
});
|
|
|
|
it('returns null if index.yaml in response is invalid', async () => {
|
|
const res = {
|
|
body: codeBlock`
|
|
some
|
|
invalid:
|
|
[
|
|
yaml
|
|
`,
|
|
};
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200, res);
|
|
const releases = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'non_existent_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
});
|
|
expect(releases).toBeNull();
|
|
});
|
|
|
|
it('returns null if packageName is not in index.yaml', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200, indexYaml);
|
|
const releases = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'non_existent_chart',
|
|
registryUrls: ['https://example-repository.com'],
|
|
});
|
|
expect(releases).toBeNull();
|
|
});
|
|
|
|
it('returns list of versions for normal response', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200, indexYaml);
|
|
const releases = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['https://example-repository.com'],
|
|
});
|
|
expect(releases).toMatchObject({
|
|
releases: [
|
|
{
|
|
newDigest:
|
|
'aa09c62be843190cc85736ba59d6411579d83ba30e9305e6b2420ea013bb5979',
|
|
version: '1.0.0',
|
|
},
|
|
{
|
|
newDigest:
|
|
'01da3c15cdec999b5afd73ee9186c62859c35a716688359c425fc04100a22144',
|
|
releaseTimestamp: '2019-02-14T15:25:43.743Z',
|
|
version: '1.1.0',
|
|
},
|
|
{ version: '1.1.1' },
|
|
{ version: '1.1.2' },
|
|
{ version: '1.1.3' },
|
|
{ version: '1.1.4' },
|
|
{ version: '1.1.5' },
|
|
{ version: '2.0.0' },
|
|
{ version: '2.0.1' },
|
|
{ version: '2.0.2' },
|
|
{ version: '2.1.0' },
|
|
{ version: '2.2.0' },
|
|
{ version: '2.2.1' },
|
|
{ version: '2.2.2' },
|
|
{ version: '2.2.3' },
|
|
{ version: '2.2.4' },
|
|
{ version: '2.2.5' },
|
|
{ version: '2.3.0' },
|
|
{ version: '2.3.1' },
|
|
{ version: '2.4.0' },
|
|
{ version: '2.4.1' },
|
|
{ version: '2.5.0' },
|
|
{ version: '2.5.1' },
|
|
{ version: '2.6.0' },
|
|
{ version: '2.6.1' },
|
|
{ version: '2.6.2' },
|
|
{ version: '2.7.0' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns list of versions for other packages if one packages has no versions', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200, Fixtures.get('index_emptypackage.yaml'));
|
|
const releases = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['https://example-repository.com'],
|
|
});
|
|
expect(releases).toMatchObject({
|
|
homepage: 'https://www.getambassador.io/',
|
|
registryUrl: 'https://example-repository.com',
|
|
sourceUrl: 'https://github.com/datawire/ambassador',
|
|
releases: expect.toBeArrayOfSize(1),
|
|
});
|
|
});
|
|
|
|
it('adds trailing slash to subdirectories', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/subdir/index.yaml')
|
|
.reply(200, indexYaml);
|
|
const res = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['https://example-repository.com/subdir'],
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
homepage: 'https://www.getambassador.io/',
|
|
registryUrl: 'https://example-repository.com/subdir',
|
|
sourceUrl: 'https://github.com/datawire/ambassador',
|
|
releases: expect.toBeArrayOfSize(27),
|
|
});
|
|
});
|
|
|
|
it('uses undefined as the newDigest when no digest is provided', async () => {
|
|
httpMock
|
|
.scope('https://example-repository.com')
|
|
.get('/index.yaml')
|
|
.reply(200, Fixtures.get('index_blank-digest.yaml'));
|
|
const releases = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'blank-digest',
|
|
registryUrls: ['https://example-repository.com'],
|
|
});
|
|
expect(releases).toMatchObject({
|
|
registryUrl: 'https://example-repository.com',
|
|
releases: [
|
|
{
|
|
newDigest: undefined,
|
|
releaseTimestamp: '2023-09-05T13:24:19.046Z',
|
|
version: '3.2.1',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('S3', () => {
|
|
const s3mock = mockClient(S3Client);
|
|
|
|
// The AWS SDK puts the error code in `name`, not `message`
|
|
function s3Error(
|
|
name: string,
|
|
message: string,
|
|
metadata?: { httpStatusCode: number },
|
|
): Error {
|
|
const err = Object.assign(new Error(message), { $metadata: metadata });
|
|
err.name = name;
|
|
return err;
|
|
}
|
|
|
|
afterEach(() => {
|
|
s3mock.reset();
|
|
hostRules.clear();
|
|
});
|
|
|
|
it('returns releases from an S3 bucket', async () => {
|
|
s3mock
|
|
.on(GetObjectCommand, {
|
|
Bucket: 'chart-bucket',
|
|
Key: 'charts/index.yaml',
|
|
})
|
|
.resolvesOnce({ Body: Readable.from([indexYaml]) as never });
|
|
|
|
const res = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
homepage: 'https://www.getambassador.io/',
|
|
registryUrl: 's3://chart-bucket/charts',
|
|
sourceUrl: 'https://github.com/datawire/ambassador',
|
|
releases: expect.toBeArrayOfSize(27),
|
|
});
|
|
});
|
|
|
|
it('uses credentials from host rules', async () => {
|
|
hostRules.add({
|
|
hostType: HelmDatasource.id,
|
|
matchHost: 'chart-bucket',
|
|
username: 'some-access-key',
|
|
password: 'some-secret-key',
|
|
token: 'some-session-token',
|
|
});
|
|
s3mock
|
|
.on(GetObjectCommand, {
|
|
Bucket: 'chart-bucket',
|
|
Key: 'charts/index.yaml',
|
|
})
|
|
.resolvesOnce({ Body: Readable.from([indexYaml]) as never });
|
|
|
|
const res = await getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
});
|
|
|
|
expect(res).toMatchObject({ registryUrl: 's3://chart-bucket/charts' });
|
|
const client = s3mock.call(0).thisValue as S3Client;
|
|
await expect(client.config.credentials()).resolves.toMatchObject({
|
|
accessKeyId: 'some-access-key',
|
|
secretAccessKey: 'some-secret-key',
|
|
sessionToken: 'some-session-token',
|
|
});
|
|
});
|
|
|
|
it('returns null when the S3 object is missing', async () => {
|
|
s3mock
|
|
.on(GetObjectCommand)
|
|
.rejectsOnce(s3Error('NoSuchKey', 'The specified key does not exist.'));
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null for an unsupported response body', async () => {
|
|
s3mock.on(GetObjectCommand).resolvesOnce({ Body: undefined });
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null when the S3 object is not found', async () => {
|
|
s3mock.on(GetObjectCommand).rejectsOnce(s3Error('NotFound', 'Not Found'));
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null when the S3 object is deleted', async () => {
|
|
s3mock.on(GetObjectCommand).resolvesOnce({ DeleteMarker: true });
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it('throws for credentials errors', async () => {
|
|
s3mock
|
|
.on(GetObjectCommand)
|
|
.rejectsOnce(
|
|
s3Error(
|
|
'CredentialsProviderError',
|
|
'Could not load credentials from any providers',
|
|
),
|
|
);
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('throws when the AWS region is missing', async () => {
|
|
s3mock
|
|
.on(GetObjectCommand)
|
|
.rejectsOnce(s3Error('Error', 'Region is missing'));
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('throws when the bucket rejects the credentials', async () => {
|
|
s3mock
|
|
.on(GetObjectCommand)
|
|
.rejectsOnce(
|
|
s3Error(
|
|
'InvalidAccessKeyId',
|
|
'The Access Key Id you provided does not exist in our records.',
|
|
{ httpStatusCode: 403 },
|
|
),
|
|
);
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('throws for unknown S3 errors', async () => {
|
|
s3mock
|
|
.on(GetObjectCommand)
|
|
.rejectsOnce(s3Error('AggregateError', 'connect ECONNREFUSED'));
|
|
|
|
await expect(
|
|
getPkgReleases({
|
|
datasource: HelmDatasource.id,
|
|
packageName: 'ambassador',
|
|
registryUrls: ['s3://chart-bucket/charts'],
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
});
|
|
});
|