renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
183 lines (182 loc) • 9.41 kB
JavaScript
import { regEx } from "../../../util/regex.js";
import { logger } from "../../../logger/index.js";
import { getQueryString, joinUrlParts } from "../../../util/url.js";
import { ExternalHostError } from "../../../types/errors/external-host-error.js";
import { id } from "../../versioning/hashicorp/index.js";
import { withCache } from "../../../util/cache/package/with-cache.js";
import { map } from "../../../util/promises.js";
import { TerraformDatasource } from "../terraform-module/base.js";
import { createSDBackendURL } from "../terraform-module/utils.js";
import { OpenTofuProviderDocsResponse, TerraformProviderReleaseBackend, TerraformProviderV2Response, TerraformProviderVersions, TerraformRegistryBuildResponse, TerraformRegistryVersions, VersionDetailResponse } from "./schema.js";
//#region lib/modules/datasource/terraform-provider/index.ts
var TerraformProviderDatasource = class TerraformProviderDatasource extends TerraformDatasource {
static id = "terraform-provider";
static hashicorpReleaseUrl = "https://releases.hashicorp.com";
static defaultRegistryUrls = [TerraformProviderDatasource.terraformRegistryUrl, TerraformProviderDatasource.hashicorpReleaseUrl];
static repositoryRegex = regEx(/^hashicorp\/(?<packageName>\S+)$/);
constructor() {
super(TerraformProviderDatasource.id);
}
defaultRegistryUrls = TerraformProviderDatasource.defaultRegistryUrls;
defaultVersioning = id;
registryStrategy = "hunt";
releaseTimestampSupport = true;
releaseTimestampNote = "The release timestamp is only available for `registry.terraform.io` (v2 API) and `registry.opentofu.org` (via `api.opentofu.org`). Other registries using the Provider Registry Protocol do not provide timestamps.";
sourceUrlSupport = "package";
sourceUrlNote = "For `registry.terraform.io`, the source URL is taken from the `source` field of the v2 API response. For `registry.opentofu.org`, it is derived from the package name following the OpenTofu registry policy of `github.com/NAMESPACE/terraform-provider-NAME`.";
async _getReleases({ packageName, registryUrl }) {
/* v8 ignore next 3 -- should never happen */
if (!registryUrl) return null;
logger.trace(`terraform-provider.getDependencies() packageName: ${packageName}`);
if (registryUrl === TerraformProviderDatasource.terraformRegistryUrl) return await this.queryTerraformRegistryV2(registryUrl, packageName);
if (registryUrl === TerraformProviderDatasource.openTofuRegistryUrl || registryUrl === TerraformProviderDatasource.openTofuApiUrl) return await this.queryOpenTofuRegistry(packageName);
if (registryUrl === TerraformProviderDatasource.hashicorpReleaseUrl) return await this.queryReleaseBackend(packageName, registryUrl);
return await this.queryProviderRegistry(registryUrl, packageName);
}
getReleases(config) {
const url = config.registryUrl;
const repo = TerraformProviderDatasource.getRepository(config);
return withCache({
namespace: `datasource-${TerraformProviderDatasource.id}`,
key: `getReleases:${url}/${repo}`,
fallback: true
}, () => this._getReleases(config));
}
static getRepository({ packageName }) {
return packageName.includes("/") ? packageName : `hashicorp/${packageName}`;
}
/**
* Query the Terraform Registry using the undocumented v2 JSON:API.
*
* Returns release timestamps for all versions, unlike the v1 API
* which only exposed the timestamp for the latest version.
*/
async queryTerraformRegistryV2(registryUrl, packageName) {
const repository = TerraformProviderDatasource.getRepository({ packageName });
const providerUrl = `${joinUrlParts(registryUrl, "v2/providers", repository)}?${getQueryString({ include: "provider-versions" })}`;
const { body: res } = await this.http.getJson(providerUrl, TerraformProviderV2Response);
res.homepage = `${registryUrl}/providers/${repository}`;
return res;
}
/**
* Query the OpenTofu registry docs API.
* https://api.opentofu.org/
*
* Used when the registry URL is `registry.opentofu.org`.
* Queries `api.opentofu.org` for provider versions with release timestamps.
*/
async queryOpenTofuRegistry(packageName) {
const repository = TerraformProviderDatasource.getRepository({ packageName });
const docsUrl = joinUrlParts(TerraformProviderDatasource.openTofuApiUrl, "registry/docs/providers", repository, "index.json");
const { body: res } = await this.http.getJson(docsUrl, OpenTofuProviderDocsResponse);
res.homepage = `https://search.opentofu.org/provider/${repository}`;
const [namespace, name] = repository.split("/");
res.sourceUrl = `https://github.com/${namespace}/terraform-provider-${name}`;
return res;
}
/**
* Query a registry using the Provider Registry Protocol that all registries
* are required to implement.
* https://www.terraform.io/internals/provider-registry-protocol
*/
async queryProviderRegistry(registryUrl, packageName) {
const repository = TerraformProviderDatasource.getRepository({ packageName });
const backendURL = createSDBackendURL(registryUrl, "providers.v1", await this.getTerraformServiceDiscoveryResult(registryUrl), `${repository}/versions`);
return { releases: (await this.http.getJson(backendURL, TerraformProviderVersions)).body.versions.map(({ version }) => ({ version })) };
}
async queryReleaseBackend(packageName, registryURL) {
const backendLookUpName = `terraform-provider-${packageName.replace("hashicorp/", "")}`;
const backendURL = joinUrlParts(registryURL, backendLookUpName, `index.json`);
const res = (await this.http.getJson(backendURL, TerraformProviderReleaseBackend)).body;
return {
releases: Object.keys(res.versions).map((version) => ({ version })),
sourceUrl: joinUrlParts("https://github.com/terraform-providers", backendLookUpName)
};
}
async _getBuilds(registryURL, repository, version) {
if (registryURL === TerraformProviderDatasource.hashicorpReleaseUrl) {
const repositoryRegexResult = TerraformProviderDatasource.repositoryRegex.exec(repository)?.groups;
if (!repositoryRegexResult) return null;
const backendLookUpName = `terraform-provider-${repositoryRegexResult.packageName}`;
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}`);
throw new ExternalHostError(err);
}
return versionReleaseBackend.builds;
}
const serviceDiscovery = await this.getTerraformServiceDiscoveryResult(registryURL);
if (!serviceDiscovery) throw new ExternalHostError(/* @__PURE__ */ new Error(`Service discovery not found for ${registryURL}`));
const backendURL = createSDBackendURL(registryURL, "providers.v1", serviceDiscovery, repository);
const versionsResponse = (await this.http.getJson(`${backendURL}/versions`, TerraformRegistryVersions)).body;
if (!versionsResponse.versions) throw new ExternalHostError(/* @__PURE__ */ new Error(`Failed to retrieve version list for ${backendURL}`));
const builds = versionsResponse.versions.find((value) => value.version === version);
if (!builds) throw new ExternalHostError(/* @__PURE__ */ new Error(`No builds found for ${repository}:${version} on ${registryURL}`));
return await map(builds.platforms, async (platform) => {
const buildURL = `${backendURL}/${version}/download/${platform.os}/${platform.arch}`;
try {
const res = (await this.http.getJson(buildURL, TerraformRegistryBuildResponse)).body;
return {
name: repository,
url: res.download_url,
version,
...res
};
} catch (err) {
/* v8 ignore next 3 -- hard to test */
if (err instanceof ExternalHostError) throw err;
logger.debug({
err,
url: buildURL
}, "Failed to retrieve build");
throw new ExternalHostError(err);
}
}, { concurrency: 4 });
}
getBuilds(registryURL, repository, version) {
return withCache({
namespace: `datasource-${TerraformProviderDatasource.id}`,
key: `getBuilds:${registryURL}/${repository}/${version}`
}, () => this._getBuilds(registryURL, repository, version));
}
async _getZipHashes(zipHashUrl) {
let rawHashData;
try {
rawHashData = (await this.http.getText(zipHashUrl)).body;
} catch (err) {
/* v8 ignore next 3 -- hard to test */
if (err instanceof ExternalHostError) throw err;
logger.debug({
err,
zipHashUrl
}, `Failed to retrieve zip hashes from ${zipHashUrl}`);
return;
}
return rawHashData.trimEnd().split("\n").map((line) => line.split(/\s/)[0]);
}
getZipHashes(zipHashUrl) {
return withCache({
namespace: `datasource-${TerraformProviderDatasource.id}`,
key: `getZipHashes:${zipHashUrl}`
}, () => this._getZipHashes(zipHashUrl));
}
async _getReleaseBackendIndex(backendLookUpName, version) {
return (await this.http.getJson(`${TerraformProviderDatasource.hashicorpReleaseUrl}/${backendLookUpName}/${version}/index.json`, VersionDetailResponse)).body;
}
getReleaseBackendIndex(backendLookUpName, version) {
return withCache({
namespace: `datasource-${TerraformProviderDatasource.id}`,
key: `getReleaseBackendIndex:${backendLookUpName}/${version}`
}, () => this._getReleaseBackendIndex(backendLookUpName, version));
}
};
//#endregion
export { TerraformProviderDatasource };
//# sourceMappingURL=index.js.map