Files
Norman 4e1553583c feat(terraform): add OCI protocol support for modules and providers (#38934)
* feat(terraform): add OCI protocol support for modules and providers

Adds support for Terraform modules and providers hosted in OCI-compatible
container registries using the `oci://` protocol.

Delegates OCI sources to Docker datasource for version resolution.
Supports both version in separate field and embedded in URL.

Closes https://github.com/renovatebot/renovate/discussions/38927

* refactor(terraform): address code review feedback for OCI support

cd - Move ociRefMatchRegex to shared location in base.ts
cd - Extract OCI parsing into parseOciRef function to reduce duplication
cd - Remove unnecessary registryUrls assignments
cd - Add skipReason when OCI tag is unspecified
cd - Inline oci.tf fixture to avoid triggering vulnerability scanners
cd - Refactor OCI regex tests to use it.each

* refactor(terraform): address PR review, round 2

- Replace custom ociRefMatchRegex/parseOciRef with getDep pattern,
reusing isOCIRegistry/removeOCIPrefix from helmv3/oci.ts
- Remove docs/development/oci-terraform-support.md
- Remove ociRefMatchRegex/parseOciRef from base.ts and modules.ts
- Simplify OCI test in extract.spec.ts
- Remove ociRefMatchRegex describe block from modules.spec.ts

* refactor(terraform): address PR review, round 3

extract duplicate function

Signed-off-by: Norman Stetter <85173861+norman-zon@users.noreply.github.com>

* fix: only allow valid oci urls

oci sources only support query params for versioning

* feat: pass through registryaliases

* fix: fix broken tests

* fix: address PR review round 4

* fix: replace new URL with parseUrl

Signed-off-by: Norman Stetter <85173861+norman-zon@users.noreply.github.com>

* fix: add tests for new path

Signed-off-by: Norman Stetter <85173861+norman-zon@users.noreply.github.com>

---------

Signed-off-by: Norman Stetter <85173861+norman-zon@users.noreply.github.com>
2026-04-29 10:57:45 +00:00

103 lines
3.3 KiB
TypeScript

import { isNonEmptyString } from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import { regEx } from '../../../util/regex.ts';
import { TerraformProviderDatasource } from '../../datasource/terraform-provider/index.ts';
import { isOCIRegistry } from '../helmv3/oci.ts';
import type { ExtractConfig, PackageDependency } from '../types.ts';
import type { TerraformDefinitionFile } from './hcl/types.ts';
import type { ProviderLock } from './lockfile/types.ts';
import {
applyOciDependency,
getLockedVersion,
massageProviderLookupName,
} from './util.ts';
export abstract class DependencyExtractor {
/**
* Get a list of signals which can be used to scan for potential processable content
* @return a list of content signals
*/
abstract getCheckList(): string[];
/**
* Extract dependencies from a HCL object
* @param hclRoot HCL parsing artifact.
* @param locks currently existing locks
*/
abstract extract(
hclRoot: TerraformDefinitionFile,
locks: ProviderLock[],
config: ExtractConfig,
): PackageDependency[];
}
export abstract class TerraformProviderExtractor extends DependencyExtractor {
sourceExtractionRegex = regEx(
/^(?:(?<hostname>(?:[a-zA-Z0-9-_]+\.+)+[a-zA-Z0-9-_]+)\/)?(?:(?<namespace>[^/]+)\/)?(?<type>[^/]+)/,
);
protected analyzeTerraformProvider(
dep: PackageDependency,
locks: ProviderLock[],
depType: string,
config: ExtractConfig,
): PackageDependency {
dep.depType = depType;
dep.depName = dep.managerData?.moduleName;
dep.datasource = TerraformProviderDatasource.id;
if (isNonEmptyString(dep.managerData?.source)) {
// TODO #22198
if (isOCIRegistry(dep.managerData.source)) {
applyOciDependency(dep, dep.managerData.source, config.registryAliases);
return dep;
}
const source = this.sourceExtractionRegex.exec(dep.managerData.source);
if (!source?.groups) {
dep.skipReason = 'unsupported-url';
return dep;
}
// buildin providers https://github.com/terraform-providers
if (source.groups.namespace === 'terraform-providers') {
dep.registryUrls = [TerraformProviderDatasource.hashicorpReleaseUrl];
} else if (source.groups.hostname) {
dep.registryUrls = [`https://${source.groups.hostname}`];
dep.packageName = `${source.groups.namespace}/${source.groups.type}`;
} else {
dep.packageName = dep.managerData?.source;
const foundLocks = locks.filter(
(lock) => lock.packageName === dep.packageName,
);
if (
foundLocks.length === 1 &&
foundLocks[0].registryUrl !==
TerraformProviderDatasource.terraformRegistryUrl
) {
logger.debug(
{ dep, foundLocks },
'Terraform: Single lock found for provider with non-default registry URL',
);
dep.registryUrls = [foundLocks[0].registryUrl];
} else if (foundLocks.length > 1) {
logger.debug(
{ dep, foundLocks },
'Terraform: Multiple locks found for provider unable to determine registry URL',
);
}
}
}
massageProviderLookupName(dep);
dep.lockedVersion = getLockedVersion(dep, locks);
if (!dep.currentValue) {
dep.skipReason = 'unspecified-version';
}
return dep;
}
}