diff --git a/lib/modules/manager/terraform/__fixtures__/azureDevOpsModules.tf b/lib/modules/manager/terraform/__fixtures__/azureDevOpsModules.tf new file mode 100644 index 0000000000000000000000000000000000000000..106bfdda44104a4f15b423adaa901169fbe61309 --- /dev/null +++ b/lib/modules/manager/terraform/__fixtures__/azureDevOpsModules.tf @@ -0,0 +1,11 @@ +module "foobar" { + source = "git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository?ref=v1.0.0" +} + +module "gittags" { + source = "git::git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository?ref=v1.0.0" +} + +module "gittags_subdir" { + source = "git::git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository//some-module/path?ref=v1.0.0" +} diff --git a/lib/modules/manager/terraform/extract.spec.ts b/lib/modules/manager/terraform/extract.spec.ts index ff8c7001d5497af3230a63e47fc4ebd807ffa6cb..d1d82d8556787a9edf81d2a4b35f94e9befac82a 100644 --- a/lib/modules/manager/terraform/extract.spec.ts +++ b/lib/modules/manager/terraform/extract.spec.ts @@ -1,23 +1,25 @@ import { join } from 'upath'; -import { fs, loadFixture } from '../../../../test/util'; +import { Fixtures } from '../../../../test/fixtures'; +import { fs } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import { extractPackageFile } from '.'; -const modules = loadFixture('modules.tf'); -const bitbucketModules = loadFixture('bitbucketModules.tf'); -const providers = loadFixture('providers.tf'); -const docker = loadFixture('docker.tf'); +const modules = Fixtures.get('modules.tf'); +const bitbucketModules = Fixtures.get('bitbucketModules.tf'); +const azureDevOpsModules = Fixtures.get('azureDevOpsModules.tf'); +const providers = Fixtures.get('providers.tf'); +const docker = Fixtures.get('docker.tf'); const tf2 = `module "relative" { source = "../fe" } `; -const helm = loadFixture('helm.tf'); -const lockedVersion = loadFixture('lockedVersion.tf'); -const lockedVersionLockfile = loadFixture('rangeStrategy.hcl'); -const terraformBlock = loadFixture('terraformBlock.tf'); -const tfeWorkspaceBlock = loadFixture('tfeWorkspace.tf'); +const helm = Fixtures.get('helm.tf'); +const lockedVersion = Fixtures.get('lockedVersion.tf'); +const lockedVersionLockfile = Fixtures.get('rangeStrategy.hcl'); +const terraformBlock = Fixtures.get('terraformBlock.tf'); +const tfeWorkspaceBlock = Fixtures.get('tfeWorkspace.tf'); const adminConfig: RepoGlobalConfig = { // `join` fixes Windows CI @@ -52,6 +54,42 @@ describe('modules/manager/terraform/extract', () => { expect(res).toMatchSnapshot(); }); + it('extracts azureDevOps modules', async () => { + const res = await extractPackageFile( + azureDevOpsModules, + 'modules.tf', + {} + ); + expect(res).toEqual({ + deps: [ + { + currentValue: 'v1.0.0', + datasource: 'git-tags', + depName: 'MyOrg/MyProject/MyRepository', + depType: 'module', + packageName: + 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }, + { + currentValue: 'v1.0.0', + datasource: 'git-tags', + depName: 'MyOrg/MyProject/MyRepository', + depType: 'module', + packageName: + 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }, + { + currentValue: 'v1.0.0', + datasource: 'git-tags', + depName: 'MyOrg/MyProject/MyRepository//some-module/path', + depType: 'module', + packageName: + 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }, + ], + }); + }); + it('extracts providers', async () => { const res = await extractPackageFile(providers, 'providers.tf', {}); expect(res.deps).toHaveLength(14); diff --git a/lib/modules/manager/terraform/modules.spec.ts b/lib/modules/manager/terraform/modules.spec.ts index 984b8189297818b601d8ed20ec0201451d8a6134..53b8d43bb1651c3a86cc5a74acfb61263b4b9ee8 100644 --- a/lib/modules/manager/terraform/modules.spec.ts +++ b/lib/modules/manager/terraform/modules.spec.ts @@ -1,4 +1,5 @@ import { + azureDevOpsSshRefMatchRegex, bitbucketRefMatchRegex, gitTagsRefMatchRegex, githubRefMatchRegex, @@ -115,4 +116,66 @@ describe('modules/manager/terraform/modules', () => { expect(dots.tag).toBe('v1.0.0'); }); }); + + describe('azureDevOpsSshRefMatchRegex', () => { + it('should split organization, project, repository and tag from source url', () => { + const ssh = azureDevOpsSshRefMatchRegex.exec( + 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository?ref=1.0.0' + )?.groups; + + expect(ssh).toEqual({ + modulepath: '', + organization: 'MyOrg', + project: 'MyProject', + repository: 'MyRepository', + tag: '1.0.0', + url: 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }); + }); + + it('should split organization, project, repository and tag from source url with git prefix', () => { + const sshGit = azureDevOpsSshRefMatchRegex.exec( + 'git::git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository?ref=1.0.0' + )?.groups; + + expect(sshGit).toEqual({ + modulepath: '', + organization: 'MyOrg', + project: 'MyProject', + repository: 'MyRepository', + tag: '1.0.0', + url: 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }); + }); + + it('should split organization, project, repository and tag from source url with subfolder', () => { + const subfolder = azureDevOpsSshRefMatchRegex.exec( + 'git::git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository//some-module/path?ref=1.0.0' + )?.groups; + + expect(subfolder).toEqual({ + modulepath: '//some-module/path', + organization: 'MyOrg', + project: 'MyProject', + repository: 'MyRepository', + tag: '1.0.0', + url: 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }); + }); + + it('should parse alpha-numeric characters as well as dots, underscores, and dashes in repo names', () => { + const dots = azureDevOpsSshRefMatchRegex.exec( + 'git::git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository//some-module/path?ref=v1.0.0' + )?.groups; + + expect(dots).toEqual({ + modulepath: '//some-module/path', + organization: 'MyOrg', + project: 'MyProject', + repository: 'MyRepository', + tag: 'v1.0.0', + url: 'git@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepository', + }); + }); + }); }); diff --git a/lib/modules/manager/terraform/modules.ts b/lib/modules/manager/terraform/modules.ts index cbb41c017f2367d5c7ba52aea7aabfd2f11c0f85..b90073a5d0a9e0a7a81e9f2fd48f7479d246cd17 100644 --- a/lib/modules/manager/terraform/modules.ts +++ b/lib/modules/manager/terraform/modules.ts @@ -18,6 +18,9 @@ export const bitbucketRefMatchRegex = regEx( export const gitTagsRefMatchRegex = regEx( /(?:git::)?(?<url>(?:http|https|ssh):\/\/(?:.*@)?(?<path>.*.*\/(?<project>.*\/.*)))\?ref=(?<tag>.*)$/ ); +export const azureDevOpsSshRefMatchRegex = regEx( + /(?:git::)?(?<url>git@ssh\.dev\.azure\.com:v3\/(?<organization>[^/]*)\/(?<project>[^/]*)\/(?<repository>[^/]*))(?<modulepath>.*)?\?ref=(?<tag>.*)$/ +); const hostnameMatchRegex = regEx(/^(?<hostname>([\w|\d]+\.)+[\w|\d]+)/); export function extractTerraformModule( @@ -39,6 +42,7 @@ export function analyseTerraformModule(dep: PackageDependency): void { const githubRefMatch = githubRefMatchRegex.exec(source); const bitbucketRefMatch = bitbucketRefMatchRegex.exec(source); const gitTagsRefMatch = gitTagsRefMatchRegex.exec(source); + const azureDevOpsSshRefMatch = azureDevOpsSshRefMatchRegex.exec(source); if (githubRefMatch?.groups) { dep.packageName = githubRefMatch.groups.project.replace( @@ -71,6 +75,12 @@ export function analyseTerraformModule(dep: PackageDependency): void { } dep.currentValue = gitTagsRefMatch.groups.tag; dep.datasource = GitTagsDatasource.id; + } else if (azureDevOpsSshRefMatch?.groups) { + dep.depType = 'module'; + dep.depName = `${azureDevOpsSshRefMatch.groups.organization}/${azureDevOpsSshRefMatch.groups.project}/${azureDevOpsSshRefMatch.groups.repository}${azureDevOpsSshRefMatch.groups.modulepath}`; + dep.packageName = azureDevOpsSshRefMatch.groups.url; + dep.currentValue = azureDevOpsSshRefMatch.groups.tag; + dep.datasource = GitTagsDatasource.id; } else if (source) { const moduleParts = source.split('//')[0].split('/'); if (moduleParts[0] === '..') {