Skip to content

Commit

Permalink
refactor(terraform-module): convert to class-based (#10486)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee committed Jun 25, 2021
1 parent f4976c5 commit 222ef91
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 111 deletions.
4 changes: 2 additions & 2 deletions lib/datasource/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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());
29 changes: 29 additions & 0 deletions lib/datasource/terraform-module/base.ts
Original file line number Diff line number Diff line change
@@ -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`;
}
}
3 changes: 2 additions & 1 deletion lib/datasource/terraform-module/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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');
const serviceDiscoveryCustomResult: any = loadFixture(
'service-custom-discovery.json'
);

const datasource = TerraformModuleDatasource.id;
const baseUrl = 'https://registry.terraform.io';
const localTerraformEnterprisebaseUrl = 'https://terraform.foo.bar';

Expand Down
182 changes: 82 additions & 100 deletions lib/datasource/terraform-module/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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}`;
}
}
7 changes: 3 additions & 4 deletions lib/datasource/terraform-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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}`;
Expand Down
4 changes: 2 additions & 2 deletions lib/manager/terraform/modules.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions lib/manager/terragrunt/modules.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit 222ef91

Please sign in to comment.