Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 105 additions & 131 deletions lib/modules/datasource/terraform-provider/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,64 +18,77 @@ const terraformProviderDatasource = new TerraformProviderDatasource();
const primaryUrl = terraformProviderDatasource.defaultRegistryUrls[0];
const secondaryUrl = terraformProviderDatasource.defaultRegistryUrls[1];

type MockVariant = 'empty' | '404' | 'error';

function mockDefaultRegistryLookup(variant: MockVariant): void {
const primaryScope = httpMock
.scope(primaryUrl)
.get('/v1/providers/hashicorp/azurerm');
const primarySd = httpMock
.scope(primaryUrl)
.get('/.well-known/terraform.json');
const secondaryScope = httpMock
.scope(secondaryUrl)
.get('/terraform-provider-azurerm/index.json');

if (variant === 'empty') {
primaryScope.reply(200, {});
primarySd.reply(200, serviceDiscoveryResult);
secondaryScope.reply(200, {});
return;
}

if (variant === '404') {
primaryScope.reply(404);
primarySd.reply(200, serviceDiscoveryResult);
secondaryScope.reply(404);
return;
}

primaryScope.replyWithError('');
primarySd.reply(200, serviceDiscoveryResult);
secondaryScope.replyWithError('');
}

function mockThirdPartyRegistryLookup(variant: MockVariant): void {
const scope = httpMock
.scope('https://registry.company.com')
.get('/v1/providers/hashicorp/azurerm/versions');

if (variant === 'empty') {
scope.reply(200, {});
} else if (variant === '404') {
scope.reply(404);
} else {
scope.replyWithError('');
}

httpMock
.scope('https://registry.company.com')
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
}

describe('modules/datasource/terraform-provider/index', () => {
describe('getReleases', () => {
it('returns null for empty result', async () => {
httpMock
.scope(primaryUrl)
.get('/v1/providers/hashicorp/azurerm')
.reply(200, {})
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
httpMock
.scope(secondaryUrl)
.get('/terraform-provider-azurerm/index.json')
.reply(200, {});
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
}),
).toBeNull();
});
it.each`
description | variant
${'empty results'} | ${'empty'}
${'404 responses'} | ${'404'}
${'unknown errors'} | ${'error'}
`(
'returns null when both default registries return $description',
async ({ variant }) => {
mockDefaultRegistryLookup(variant);

it('returns null for 404', async () => {
httpMock
.scope(primaryUrl)
.get('/v1/providers/hashicorp/azurerm')
.reply(404)
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
httpMock
.scope(secondaryUrl)
.get('/terraform-provider-azurerm/index.json')
.reply(404);
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
}),
).toBeNull();
});

it('returns null for unknown error', async () => {
httpMock
.scope(primaryUrl)
.get('/v1/providers/hashicorp/azurerm')
.replyWithError('')
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
httpMock
.scope(secondaryUrl)
.get('/terraform-provider-azurerm/index.json')
.replyWithError('');
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
}),
).toBeNull();
});
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
}),
).toBeNull();
},
);

it('processes real data', async () => {
httpMock
Expand Down Expand Up @@ -105,53 +118,49 @@ describe('modules/datasource/terraform-provider/index', () => {
});
});

it('returns null for empty result from third party', async () => {
it('does not set releaseTimestamp when latest version is not in releases', async () => {
httpMock
.scope('https://registry.company.com')
.get('/v1/providers/hashicorp/azurerm/versions')
.reply(200, {})
.scope(primaryUrl)
.get('/v1/providers/hashicorp/azurerm')
.reply(200, {
source: 'https://github.com/hashicorp/terraform-provider-azurerm',
versions: ['2.52.0'],
version: '9.9.9',
published_at: '2019-11-26T08:22:56Z',
})
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
registryUrls: ['https://registry.company.com'],
}),
).toBeNull();
const res = await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
});
expect(res).toEqual({
homepage: 'https://registry.terraform.io/providers/hashicorp/azurerm',
registryUrl: 'https://registry.terraform.io',
releases: [{ version: '2.52.0' }],
sourceUrl: 'https://github.com/hashicorp/terraform-provider-azurerm',
});
});

it('returns null for 404 from third party', async () => {
httpMock
.scope('https://registry.company.com')
.get('/v1/providers/hashicorp/azurerm/versions')
.reply(404)
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
registryUrls: ['https://registry.company.com'],
}),
).toBeNull();
});
it.each`
description | variant
${'empty results'} | ${'empty'}
${'404 responses'} | ${'404'}
${'unknown errors'} | ${'error'}
`(
'returns null when a third-party registry returns $description',
async ({ variant }) => {
mockThirdPartyRegistryLookup(variant);

it('returns null for unknown error from third party', async () => {
httpMock
.scope('https://registry.company.com')
.get('/v1/providers/hashicorp/azurerm/versions')
.replyWithError('')
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
registryUrls: ['https://registry.company.com'],
}),
).toBeNull();
});
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
registryUrls: ['https://registry.company.com'],
}),
).toBeNull();
},
);

it('processes real data from third party', async () => {
httpMock
Expand Down Expand Up @@ -216,41 +225,6 @@ describe('modules/datasource/terraform-provider/index', () => {
'https://github.com/terraform-providers/terraform-provider-google-beta',
});
});

it('simulate failing secondary release source', async () => {
httpMock
.scope(primaryUrl)
.get('/v1/providers/hashicorp/datadog')
.reply(404, {
errors: ['Not Found'],
})
.get('/.well-known/terraform.json')
.reply(200, serviceDiscoveryResult);
httpMock
.scope(secondaryUrl)
.get('/terraform-provider-datadog/index.json')
.reply(404);

const res = await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'datadog',
});
expect(res).toBeNull();
});

it('returns null for error in service discovery', async () => {
httpMock.scope(primaryUrl).get('/.well-known/terraform.json').reply(404);
httpMock
.scope(secondaryUrl)
.get('/terraform-provider-azurerm/index.json')
.replyWithError('');
expect(
await getPkgReleases({
datasource: TerraformProviderDatasource.id,
packageName: 'azurerm',
}),
).toBeNull();
});
});

describe('getBuilds', () => {
Expand Down
60 changes: 32 additions & 28 deletions lib/modules/datasource/terraform-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ import type {
export class TerraformProviderDatasource extends TerraformDatasource {
static override readonly id = 'terraform-provider';

static readonly hashicorpReleaseUrl = 'https://releases.hashicorp.com';
static readonly terraformRegistryUrl = 'https://registry.terraform.io';

static readonly defaultRegistryUrls = [
'https://registry.terraform.io',
'https://releases.hashicorp.com',
TerraformProviderDatasource.terraformRegistryUrl,
TerraformProviderDatasource.hashicorpReleaseUrl,
];

static repositoryRegex = regEx(/^hashicorp\/(?<packageName>\S+)$/);
Expand Down Expand Up @@ -61,28 +64,24 @@ export class TerraformProviderDatasource extends TerraformDatasource {
`terraform-provider.getDependencies() packageName: ${packageName}`,
);

if (registryUrl === this.defaultRegistryUrls[1]) {
return await this.queryReleaseBackend(packageName, registryUrl);
}
const repository = TerraformProviderDatasource.getRepository({
packageName,
});
const serviceDiscovery =
await this.getTerraformServiceDiscoveryResult(registryUrl);

if (registryUrl === this.defaultRegistryUrls[0]) {
return await this.queryRegistryExtendedApi(
if (registryUrl === TerraformProviderDatasource.terraformRegistryUrl) {
const repository = TerraformProviderDatasource.getRepository({
packageName,
});
const serviceDiscovery =
await this.getTerraformServiceDiscoveryResult(registryUrl);
return await this.queryTerraformRegistry(
serviceDiscovery,
registryUrl,
repository,
);
}
if (registryUrl === TerraformProviderDatasource.hashicorpReleaseUrl) {
return await this.queryReleaseBackend(packageName, registryUrl);
}

return await this.queryRegistryVersions(
serviceDiscovery,
registryUrl,
repository,
);
// Fall back to the standard Provider Registry Protocol for other registries.
return await this.queryProviderRegistry(registryUrl, packageName);
}

getReleases(config: GetReleasesConfig): Promise<ReleaseResult | null> {
Expand All @@ -103,11 +102,11 @@ export class TerraformProviderDatasource extends TerraformDatasource {
}

/**
* this uses the api that terraform registry has in addition to the base api
* this endpoint provides more information, such as release date
* this api is undocumented.
* Query the Terraform Registry using the undocumented extended provider API.
* This provides more information than the base Provider Registry Protocol,
* such as the release date for the latest version and the source URL.
*/
private async queryRegistryExtendedApi(
private async queryTerraformRegistry(
serviceDiscovery: ServiceDiscoveryResult,
registryUrl: string,
repository: string,
Expand Down Expand Up @@ -141,14 +140,19 @@ export class TerraformProviderDatasource extends TerraformDatasource {
}

/**
* this version uses the Provider Registry Protocol that all registries are required to implement
* Query a registry using the Provider Registry Protocol that all registries
* are required to implement.
* https://www.terraform.io/internals/provider-registry-protocol
*/
private async queryRegistryVersions(
serviceDiscovery: ServiceDiscoveryResult,
private async queryProviderRegistry(
registryUrl: string,
repository: string,
packageName: string,
): Promise<ReleaseResult> {
const repository = TerraformProviderDatasource.getRepository({
packageName,
});
const serviceDiscovery: ServiceDiscoveryResult =
await this.getTerraformServiceDiscoveryResult(registryUrl);
const backendURL = createSDBackendURL(
registryUrl,
'providers.v1',
Expand Down Expand Up @@ -200,7 +204,7 @@ export class TerraformProviderDatasource extends TerraformDatasource {
repository: string,
version: string,
): Promise<TerraformBuild[] | null> {
if (registryURL === TerraformProviderDatasource.defaultRegistryUrls[1]) {
if (registryURL === TerraformProviderDatasource.hashicorpReleaseUrl) {
// check if registryURL === secondary backend
const repositoryRegexResult =
TerraformProviderDatasource.repositoryRegex.exec(repository)?.groups;
Expand Down Expand Up @@ -356,7 +360,7 @@ export class TerraformProviderDatasource extends TerraformDatasource {
): Promise<VersionDetailResponse> {
return (
await this.http.getJsonUnchecked<VersionDetailResponse>(
`${TerraformProviderDatasource.defaultRegistryUrls[1]}/${backendLookUpName}/${version}/index.json`,
`${TerraformProviderDatasource.hashicorpReleaseUrl}/${backendLookUpName}/${version}/index.json`,
)
).body;
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@
"semantic-release": "25.0.3",
"tar": "7.5.13",
"tmp-promise": "3.0.3",
"tsdown": "0.21.5",
"tsdown": "0.21.6",
"type-fest": "5.5.0",
"typescript": "5.9.3",
"typescript-eslint": "8.57.2",
Expand Down
Loading
Loading