From 222ef91fe9d91110f2933e066b33bf954360e58d Mon Sep 17 00:00:00 2001 From: Jamie Magee <jamie.magee@gmail.com> Date: Fri, 25 Jun 2021 05:12:23 -0700 Subject: [PATCH] refactor(terraform-module): convert to class-based (#10486) --- lib/datasource/api.ts | 4 +- lib/datasource/terraform-module/base.ts | 29 +++ lib/datasource/terraform-module/index.spec.ts | 3 +- lib/datasource/terraform-module/index.ts | 182 ++++++++---------- lib/datasource/terraform-provider/index.ts | 7 +- lib/manager/terraform/modules.ts | 4 +- lib/manager/terragrunt/modules.ts | 4 +- 7 files changed, 122 insertions(+), 111 deletions(-) create mode 100644 lib/datasource/terraform-module/base.ts diff --git a/lib/datasource/api.ts b/lib/datasource/api.ts index 14eecf92ee..8cf8702425 100644 --- a/lib/datasource/api.ts +++ b/lib/datasource/api.ts @@ -28,7 +28,7 @@ import * as rubyVersion from './ruby-version'; import * as rubygems from './rubygems'; import * as sbtPackage from './sbt-package'; import * as sbtPlugin from './sbt-plugin'; -import * as terraformModule from './terraform-module'; +import { TerraformModuleDatasource } from './terraform-module'; import { TerraformProviderDatasource } from './terraform-provider'; import type { DatasourceApi } from './types'; @@ -65,5 +65,5 @@ api.set('ruby-version', rubyVersion); api.set('rubygems', rubygems); api.set('sbt-package', sbtPackage); api.set('sbt-plugin', sbtPlugin); -api.set('terraform-module', terraformModule); +api.set('terraform-module', new TerraformModuleDatasource()); api.set('terraform-provider', new TerraformProviderDatasource()); diff --git a/lib/datasource/terraform-module/base.ts b/lib/datasource/terraform-module/base.ts new file mode 100644 index 0000000000..688a9a4291 --- /dev/null +++ b/lib/datasource/terraform-module/base.ts @@ -0,0 +1,29 @@ +import { cache } from '../../util/cache/package/decorator'; +import { ensureTrailingSlash } from '../../util/url'; +import { Datasource } from '../datasource'; +import type { ServiceDiscoveryResult } from './types'; + +// TODO: extract to a separate directory structure (#10532) +export abstract class TerraformDatasource extends Datasource { + static id = 'terraform'; + + @cache({ + namespace: `datasource-${TerraformDatasource.id}`, + key: (registryUrl: string) => + TerraformDatasource.getDiscoveryUrl(registryUrl), + ttlMinutes: 1440, + }) + async getTerraformServiceDiscoveryResult( + registryUrl: string + ): Promise<ServiceDiscoveryResult> { + const discoveryURL = TerraformDatasource.getDiscoveryUrl(registryUrl); + const serviceDiscovery = ( + await this.http.getJson<ServiceDiscoveryResult>(discoveryURL) + ).body; + return serviceDiscovery; + } + + private static getDiscoveryUrl(registryUrl: string): string { + return `${ensureTrailingSlash(registryUrl)}.well-known/terraform.json`; + } +} diff --git a/lib/datasource/terraform-module/index.spec.ts b/lib/datasource/terraform-module/index.spec.ts index d93b9a67da..c9576339b3 100644 --- a/lib/datasource/terraform-module/index.spec.ts +++ b/lib/datasource/terraform-module/index.spec.ts @@ -1,7 +1,7 @@ import { getPkgReleases } from '..'; import * as httpMock from '../../../test/http-mock'; import { getName, loadFixture } from '../../../test/util'; -import { id as datasource } from '.'; +import { TerraformModuleDatasource } from '.'; const consulData: any = loadFixture('registry-consul.json'); const serviceDiscoveryResult: any = loadFixture('service-discovery.json'); @@ -9,6 +9,7 @@ const serviceDiscoveryCustomResult: any = loadFixture( 'service-custom-discovery.json' ); +const datasource = TerraformModuleDatasource.id; const baseUrl = 'https://registry.terraform.io'; const localTerraformEnterprisebaseUrl = 'https://terraform.foo.bar'; diff --git a/lib/datasource/terraform-module/index.ts b/lib/datasource/terraform-module/index.ts index 42c165da21..6c3f329657 100644 --- a/lib/datasource/terraform-module/index.ts +++ b/lib/datasource/terraform-module/index.ts @@ -1,112 +1,62 @@ import { logger } from '../../logger'; import { ExternalHostError } from '../../types/errors/external-host-error'; -import * as packageCache from '../../util/cache/package'; -import { Http } from '../../util/http'; +import { cache } from '../../util/cache/package/decorator'; +import type { HttpError } from '../../util/http/types'; import * as hashicorpVersioning from '../../versioning/hashicorp'; import type { GetReleasesConfig, ReleaseResult } from '../types'; -import type { - RegistryRepository, - ServiceDiscoveryResult, - TerraformRelease, -} from './types'; +import { TerraformDatasource } from './base'; +import type { RegistryRepository, TerraformRelease } from './types'; -export const id = 'terraform-module'; -export const customRegistrySupport = true; -export const defaultRegistryUrls = ['https://registry.terraform.io']; -export const defaultVersioning = hashicorpVersioning.id; -export const registryStrategy = 'first'; +export class TerraformModuleDatasource extends TerraformDatasource { + static readonly id = 'terraform-module'; -const http = new Http(id); - -function getRegistryRepository( - lookupName: string, - registryUrl: string -): RegistryRepository { - let registry: string; - const split = lookupName.split('/'); - if (split.length > 3 && split[0].includes('.')) { - [registry] = split; - split.shift(); - } else { - registry = registryUrl; - } - if (!/^https?:\/\//.test(registry)) { - registry = `https://${registry}`; + constructor() { + super(TerraformModuleDatasource.id); } - const repository = split.join('/'); - return { - registry, - repository, - }; -} -export async function getTerraformServiceDiscoveryResult( - registryUrl: string -): Promise<ServiceDiscoveryResult> { - const discoveryURL = `${registryUrl}/.well-known/terraform.json`; - const cacheNamespace = 'terraform-service-discovery'; - const cachedResult = await packageCache.get<ServiceDiscoveryResult>( - cacheNamespace, - registryUrl - ); - // istanbul ignore if - if (cachedResult) { - return cachedResult; - } - const serviceDiscovery = ( - await http.getJson<ServiceDiscoveryResult>(discoveryURL) - ).body; + readonly defaultRegistryUrls = ['https://registry.terraform.io']; - const cacheMinutes = 1440; // 24h - await packageCache.set( - cacheNamespace, - registryUrl, - serviceDiscovery, - cacheMinutes - ); + readonly defaultVersioning = hashicorpVersioning.id; - return serviceDiscovery; -} -/** - * terraform.getReleases - * - * This function will fetch a package from the specified Terraform registry and return all semver versions. - * - `sourceUrl` is supported of "source" field is set - * - `homepage` is set to the Terraform registry's page if it's on the official main registry - */ -export async function getReleases({ - lookupName, - registryUrl, -}: GetReleasesConfig): Promise<ReleaseResult | null> { - const { registry, repository } = getRegistryRepository( + /** + * This function will fetch a package from the specified Terraform registry and return all semver versions. + * - `sourceUrl` is supported of "source" field is set + * - `homepage` is set to the Terraform registry's page if it's on the official main registry + */ + @cache({ + namespace: `datasource-${TerraformModuleDatasource.id}`, + key: (getReleasesConfig: GetReleasesConfig) => + TerraformModuleDatasource.getCacheKey(getReleasesConfig), + }) + async getReleases({ lookupName, - registryUrl - ); - logger.debug( - { registry, terraformRepository: repository }, - 'terraform.getDependencies()' - ); - const cacheNamespace = 'terraform-module'; - const cacheURL = `${registry}/${repository}`; - const cachedResult = await packageCache.get<ReleaseResult>( - cacheNamespace, - cacheURL - ); - // istanbul ignore if - if (cachedResult) { - return cachedResult; - } - try { - const serviceDiscovery = await getTerraformServiceDiscoveryResult( - registryUrl + registryUrl, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + const { registry, repository } = + TerraformModuleDatasource.getRegistryRepository(lookupName, registryUrl); + logger.trace( + { registry, terraformRepository: repository }, + 'terraform-module.getReleases()' ); - const pkgUrl = `${registry}${serviceDiscovery['modules.v1']}${repository}`; - const res = (await http.getJson<TerraformRelease>(pkgUrl)).body; - const returnedName = res.namespace + '/' + res.name + '/' + res.provider; - if (returnedName !== repository) { - logger.warn({ pkgUrl }, 'Terraform registry result mismatch'); - return null; + + let res: TerraformRelease; + let pkgUrl: string; + + try { + const serviceDiscovery = await this.getTerraformServiceDiscoveryResult( + registryUrl + ); + pkgUrl = `${registry}${serviceDiscovery['modules.v1']}${repository}`; + res = (await this.http.getJson<TerraformRelease>(pkgUrl)).body; + const returnedName = res.namespace + '/' + res.name + '/' + res.provider; + if (returnedName !== repository) { + logger.warn({ pkgUrl }, 'Terraform registry result mismatch'); + return null; + } + } catch (err) { + this.handleGenericErrors(err); } + // Simplify response before caching and returning const dep: ReleaseResult = { releases: null, @@ -127,16 +77,48 @@ export async function getReleases({ if (latestVersion) { latestVersion.releaseTimestamp = res.published_at; } + logger.trace({ dep }, 'dep'); - const cacheMinutes = 30; - await packageCache.set(cacheNamespace, pkgUrl, dep, cacheMinutes); return dep; - } catch (err) { + } + + // eslint-disable-next-line class-methods-use-this + override handleSpecificErrors(err: HttpError): void { const failureCodes = ['EAI_AGAIN']; // istanbul ignore if if (failureCodes.includes(err.code)) { throw new ExternalHostError(err); } - throw err; + } + + private static getRegistryRepository( + lookupName: string, + registryUrl: string + ): RegistryRepository { + let registry: string; + const split = lookupName.split('/'); + if (split.length > 3 && split[0].includes('.')) { + [registry] = split; + split.shift(); + } else { + registry = registryUrl; + } + if (!/^https?:\/\//.test(registry)) { + registry = `https://${registry}`; + } + const repository = split.join('/'); + return { + registry, + repository, + }; + } + + private static getCacheKey({ + lookupName, + registryUrl, + }: GetReleasesConfig): string { + const { registry, repository } = + TerraformModuleDatasource.getRegistryRepository(lookupName, registryUrl); + return `${registry}/${repository}`; } } diff --git a/lib/datasource/terraform-provider/index.ts b/lib/datasource/terraform-provider/index.ts index 07e401b8f7..e03b488bec 100644 --- a/lib/datasource/terraform-provider/index.ts +++ b/lib/datasource/terraform-provider/index.ts @@ -2,15 +2,14 @@ import { logger } from '../../logger'; import { cache } from '../../util/cache/package/decorator'; import { parseUrl } from '../../util/url'; import * as hashicorpVersioning from '../../versioning/hashicorp'; -import { Datasource } from '../datasource'; -import { getTerraformServiceDiscoveryResult } from '../terraform-module'; +import { TerraformDatasource } from '../terraform-module/base'; import type { GetReleasesConfig, ReleaseResult } from '../types'; import type { TerraformProvider, TerraformProviderReleaseBackend, } from './types'; -export class TerraformProviderDatasource extends Datasource { +export class TerraformProviderDatasource extends TerraformDatasource { static readonly id = 'terraform-provider'; static readonly defaultRegistryUrls = [ @@ -63,7 +62,7 @@ export class TerraformProviderDatasource extends Datasource { registryURL: string, repository: string ): Promise<ReleaseResult> { - const serviceDiscovery = await getTerraformServiceDiscoveryResult( + const serviceDiscovery = await this.getTerraformServiceDiscoveryResult( registryURL ); const backendURL = `${registryURL}${serviceDiscovery['providers.v1']}${repository}`; diff --git a/lib/manager/terraform/modules.ts b/lib/manager/terraform/modules.ts index ef9eda0a21..a7903a0233 100644 --- a/lib/manager/terraform/modules.ts +++ b/lib/manager/terraform/modules.ts @@ -1,6 +1,6 @@ import * as datasourceGitTags from '../../datasource/git-tags'; import * as datasourceGithubTags from '../../datasource/github-tags'; -import * as datasourceTerraformModule from '../../datasource/terraform-module'; +import { TerraformModuleDatasource } from '../../datasource/terraform-module'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; import type { PackageDependency } from '../types'; @@ -61,7 +61,7 @@ export function analyseTerraformModule(dep: PackageDependency): void { } dep.depType = 'module'; dep.depName = moduleParts.join('/'); - dep.datasource = datasourceTerraformModule.id; + dep.datasource = TerraformModuleDatasource.id; } } else { logger.debug({ dep }, 'terraform dep has no source'); diff --git a/lib/manager/terragrunt/modules.ts b/lib/manager/terragrunt/modules.ts index 7b597007a5..3b3f0e4ba5 100644 --- a/lib/manager/terragrunt/modules.ts +++ b/lib/manager/terragrunt/modules.ts @@ -1,6 +1,6 @@ import * as datasourceGitTags from '../../datasource/git-tags'; import * as datasourceGithubTags from '../../datasource/github-tags'; -import * as datasourceTerragruntModule from '../../datasource/terraform-module'; +import { TerraformModuleDatasource } from '../../datasource/terraform-module'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; import type { PackageDependency } from '../types'; @@ -62,7 +62,7 @@ export function analyseTerragruntModule(dep: PackageDependency): void { } dep.depType = 'terragrunt'; dep.depName = moduleParts.join('/'); - dep.datasource = datasourceTerragruntModule.id; + dep.datasource = TerraformModuleDatasource.id; } } else { logger.debug({ dep }, 'terragrunt dep has no source'); -- GitLab