import pMap from 'p-map' ; import { logger } from '../../logger' ; import { ExternalHostError } from '../../types/errors/external-host-error' ; import { cache } from '../../util/cache/package/decorator' ; import { regEx } from '../../util/regex' ; import { parseUrl } from '../../util/url' ; import * as hashicorpVersioning from '../../versioning/hashicorp' ; import { TerraformDatasource } from '../terraform-module/base' ; import type { GetReleasesConfig , ReleaseResult } from '../types' ; import type { TerraformBuild , TerraformProvider , TerraformProviderReleaseBackend , TerraformRegistryBuildResponse , TerraformRegistryVersions , VersionDetailResponse , } from './types' ; export class TerraformProviderDatasource extends TerraformDatasource { static override readonly id = 'terraform-provider' ; static readonly defaultRegistryUrls = [ 'https://registry.terraform.io' , 'https://releases.hashicorp.com' , ] ; static repositoryRegex = regEx ( /^hashicorp\/(?<lookupName>\S+)$/ ) ; constructor ( ) { super ( TerraformProviderDatasource . id ) ; } override readonly defaultRegistryUrls = TerraformProviderDatasource . defaultRegistryUrls ; override readonly defaultVersioning = hashicorpVersioning . id ; override readonly registryStrategy = 'hunt' ; @ cache ( { namespace : `datasource-${ TerraformProviderDatasource . id }` , key : ( getReleasesConfig ) => `${ getReleasesConfig . registryUrl }/${ TerraformProviderDatasource . getRepository ( getReleasesConfig ) }` , } ) async getReleases ( { lookupName , registryUrl , } ) { logger . debug ( { lookupName } , 'terraform-provider.getDependencies()' ) ; let dep = null ; const registryHost = parseUrl ( registryUrl ) . host ; if ( registryHost === 'releases.hashicorp.com' ) { dep = await this . queryReleaseBackend ( lookupName , registryUrl ) ; } else { const repository = TerraformProviderDatasource . getRepository ( { lookupName , } ) ; dep = await this . queryRegistry ( registryUrl , repository ) ; } return dep ; } private static getRepository ( { lookupName } ) { return lookupName . includes ( '/' ) ? lookupName : `hashicorp/${ lookupName }` ; } private async queryRegistry ( registryURL , repository ) { const serviceDiscovery = await this . getTerraformServiceDiscoveryResult ( registryURL ) ; const backendURL = `${ registryURL }${ serviceDiscovery [ 'providers.v1' ] }${ repository }` ; const res = ( await this . http . getJson < TerraformProvider > ( backendURL ) ) . body ; const dep = { releases : null , } ; if ( res . source ) { dep . sourceUrl = res . source ; } dep . releases = res . versions . map ( ( version ) => ( { version , } ) ) ; const latestVersion = dep . releases . find ( ( release ) => res . version === release . version ) ; if ( latestVersion ) { latestVersion . releaseTimestamp = res . published_at ; } dep . homepage = `${ registryURL }/providers/${ repository }` ; logger . trace ( { dep } , 'dep' ) ; return dep ; } private async queryReleaseBackend ( lookupName , registryURL ) { const backendLookUpName = `terraform-provider-${ lookupName }` ; const backendURL = registryURL + `/index.json` ; const res = ( await this . http . getJson < TerraformProviderReleaseBackend > ( backendURL ) ) . body ; if ( ! res [ backendLookUpName ] ) { return null ; } const dep = { releases : null , sourceUrl : `https://github.com/terraform-providers/${ backendLookUpName }` , } ; dep . releases = Object . keys ( res [ backendLookUpName ] . versions ) . map ( ( version ) => ( { version , } ) ) ; logger . trace ( { dep } , 'dep' ) ; return dep ; } @ cache ( { namespace : `datasource-${ TerraformProviderDatasource . id }-builds` , key : ( registryURL , repository , version ) => `${ registryURL }/${ repository }/${ version }` , } ) async getBuilds ( registryURL , repository , version ) { if ( registryURL === TerraformProviderDatasource . defaultRegistryUrls [ 1 ] ) { const repositoryRegexResult = TerraformProviderDatasource . repositoryRegex . exec ( repository ) ; if ( ! repositoryRegexResult ) { return null ; } const lookupName = repositoryRegexResult . groups . lookupName ; const backendLookUpName = `terraform-provider-${ lookupName }` ; let versionReleaseBackend ; try { versionReleaseBackend = await this . getReleaseBackendIndex ( backendLookUpName , version ) ; } catch ( err ) { if ( err instanceof ExternalHostError ) { throw err ; } logger . debug ( { err , backendLookUpName , version } , `Failed to retrieve builds for ${ backendLookUpName } ${ version }` ) ; return null ; } return versionReleaseBackend . builds ; } const serviceDiscovery = await this . getTerraformServiceDiscoveryResult ( registryURL ) ; if ( ! serviceDiscovery ) { logger . trace ( `Failed to retrieve service discovery from ${ registryURL }` ) ; return null ; } const backendURL = `${ registryURL }${ serviceDiscovery [ 'providers.v1' ] }${ repository }` ; const versionsResponse = ( await this . http . getJson < TerraformRegistryVersions > ( `${ backendURL }/versions` ) ) . body ; if ( ! versionsResponse . versions ) { logger . trace ( `Failed to retrieve version list for ${ backendURL }` ) ; return null ; } const builds = versionsResponse . versions . find ( ( value ) => value . version === version ) ; if ( ! builds ) { logger . trace ( `No builds found for ${ repository }:${ version } on ${ registryURL }` ) ; return null ; } const result = await pMap ( builds . platforms , async ( platform ) => { const buildURL = `${ backendURL }/${ version }/download/${ platform . os }/${ platform . arch }` ; try { const res = ( await this . http . getJson < TerraformRegistryBuildResponse > ( buildURL ) ) . body ; const newBuild = { name : repository , url : res . download_url , version , ... res , } ; return newBuild ; } catch ( err ) { if ( err instanceof ExternalHostError ) { throw err ; } logger . debug ( { err , url : buildURL } , 'Failed to retrieve build' ) ; return null ; } } , { concurrency : 4 } ) ; if ( result . some ( ( value ) => Boolean ( value ) === false ) ) { return null ; } return result ; } @ cache ( { namespace : `datasource-${ TerraformProviderDatasource . id }-releaseBackendIndex` , key : ( backendLookUpName , version ) => `${ backendLookUpName }/${ version }` , } ) async getReleaseBackendIndex ( backendLookUpName , version ) { return ( await this . http . getJson < VersionDetailResponse > ( `${ TerraformProviderDatasource . defaultRegistryUrls [ 1 ] }/${ backendLookUpName }/${ version }/index.json` ) ) . body ; } }