diff --git a/.changeset/tall-pants-clap.md b/.changeset/tall-pants-clap.md new file mode 100644 index 0000000000..5a1714c106 --- /dev/null +++ b/.changeset/tall-pants-clap.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +--- + +Support self-hosted gitlab installations with relative URL. diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 2387dadf5b..5bc234daef 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -16,6 +16,7 @@ import { getGitLabFileFetchUrl, + getGitLabIntegrationRelativePath, getGitLabRequestOptions, GitLabIntegration, ScmIntegrations, @@ -37,7 +38,7 @@ import { ReadUrlResponse, ReadUrlOptions, } from './types'; -import { trimEnd } from 'lodash'; +import { trimEnd, trimStart } from 'lodash'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; /** @@ -117,13 +118,24 @@ export class GitlabUrlReader implements UrlReader { const { etag, signal } = options ?? {}; const { ref, full_name, filepath } = parseGitUrl(url); + let repoFullName = full_name; + + const relativePath = getGitLabIntegrationRelativePath( + this.integration.config, + ); + + // Considering self hosted gitlab with relative + if (relativePath) { + repoFullName = trimStart(full_name, relativePath); + } + // Use GitLab API to get the default branch // encodeURIComponent is required for GitLab API // https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding const projectGitlabResponse = await fetch( new URL( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( - full_name, + repoFullName, )}`, ).toString(), getGitLabRequestOptions(this.integration.config), @@ -150,7 +162,7 @@ export class GitlabUrlReader implements UrlReader { const commitsGitlabResponse = await fetch( new URL( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( - full_name, + repoFullName, )}/repository/commits?${commitsReqParams.toString()}`, ).toString(), { @@ -181,7 +193,7 @@ export class GitlabUrlReader implements UrlReader { // https://docs.gitlab.com/ee/api/repositories.html#get-file-archive const archiveGitLabResponse = await fetch( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( - full_name, + repoFullName, )}/repository/archive?sha=${branch}`, { ...getGitLabRequestOptions(this.integration.config), diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 3ffb131900..8d8d017095 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -342,6 +342,11 @@ export function getGitLabFileFetchUrl( config: GitLabIntegrationConfig, ): Promise; +// @public +export function getGitLabIntegrationRelativePath( + config: GitLabIntegrationConfig, +): string; + // @public export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { headers: Record; diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 71b4cd868f..301fe46f90 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -69,7 +69,6 @@ export function readGitLabIntegrationConfig( let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const token = config.getOptionalString('token'); let baseUrl = config.getOptionalString('baseUrl'); - if (apiBaseUrl) { apiBaseUrl = trimEnd(apiBaseUrl, '/'); } else if (host === GITLAB_HOST) { @@ -124,3 +123,20 @@ export function readGitLabIntegrationConfigs( return result; } + +/** + * Reads a GitLab integration config, and returns + * relative path. + * + * @param config - GitLabIntegrationConfig object + * @public + */ +export function getGitLabIntegrationRelativePath( + config: GitLabIntegrationConfig, +): string { + let relativePath = ''; + if (config.host !== GITLAB_HOST) { + relativePath = new URL(config.baseUrl).pathname; + } + return trimEnd(relativePath, '/'); +} diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index d01406fe1c..d043aba55a 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -35,18 +35,32 @@ describe('gitlab core', () => { }); const configWithToken: GitLabIntegrationConfig = { - host: 'g.com', + host: 'gitlab.com', token: '0123456789', apiBaseUrl: '', baseUrl: '', }; const configWithNoToken: GitLabIntegrationConfig = { - host: 'g.com', + host: 'gitlab.com', apiBaseUrl: '', baseUrl: '', }; + const configSelfHosteWithRelativePath: GitLabIntegrationConfig = { + host: 'gitlab.mycompany.com', + token: '0123456789', + apiBaseUrl: '', + baseUrl: 'https://gitlab.mycompany.com/gitlab', + }; + + const configSelfHostedWithoutRelativePath: GitLabIntegrationConfig = { + host: 'gitlab.mycompany.com', + token: '0123456789', + apiBaseUrl: '', + baseUrl: 'https://gitlab.mycompany.com', + }; + describe('getGitLabFileFetchUrl with .yaml extension', () => { it.each([ // Project URLs @@ -56,6 +70,18 @@ describe('gitlab core', () => { result: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', }, + { + config: configSelfHosteWithRelativePath, + url: 'https://gitlab.mycompany.com/gitlab/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + result: + 'https://gitlab.mycompany.com/gitlab/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, + { + config: configSelfHostedWithoutRelativePath, + url: 'https://gitlab.mycompany.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + result: + 'https://gitlab.mycompany.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, { config: configWithNoToken, // Works with non URI encoded link @@ -63,6 +89,18 @@ describe('gitlab core', () => { result: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yaml/raw?ref=branch', }, + { + config: configSelfHosteWithRelativePath, + url: 'https://gitlab.mycompany.com/gitlab/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yaml', + result: + 'https://gitlab.mycompany.com/gitlab/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yaml/raw?ref=branch', + }, + { + config: configSelfHostedWithoutRelativePath, + url: 'https://gitlab.mycompany.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yaml', + result: + 'https://gitlab.mycompany.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yaml/raw?ref=branch', + }, { config: configWithNoToken, url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', @@ -121,6 +159,18 @@ describe('gitlab core', () => { result: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yml/raw?ref=branch', }, + { + config: configSelfHosteWithRelativePath, + url: 'https://gitlab.mycompany.com/gitlab/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yml', + result: + 'https://gitlab.mycompany.com/gitlab/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yml/raw?ref=branch', + }, + { + config: configSelfHostedWithoutRelativePath, + url: 'https://gitlab.mycompany.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yml', + result: + 'https://gitlab.mycompany.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yml/raw?ref=branch', + }, { config: configWithNoToken, // Works with non URI encoded link @@ -128,6 +178,20 @@ describe('gitlab core', () => { result: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yml/raw?ref=branch', }, + { + config: configSelfHosteWithRelativePath, + // Works with non URI encoded link + url: 'https://gitlab.mycompany.com/gitlab/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yml', + result: + 'https://gitlab.mycompany.com/gitlab/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yml/raw?ref=branch', + }, + { + config: configSelfHostedWithoutRelativePath, + // Works with non URI encoded link + url: 'https://gitlab.mycompany.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yml', + result: + 'https://gitlab.mycompany.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yml/raw?ref=branch', + }, { config: configWithNoToken, url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yml', diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index ba0f9c83de..1fcf15a78f 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { GitLabIntegrationConfig } from './config'; +import { + getGitLabIntegrationRelativePath, + GitLabIntegrationConfig, +} from './config'; import fetch from 'cross-fetch'; import { InputError } from '@backstage/errors'; @@ -42,9 +45,10 @@ export async function getGitLabFileFetchUrl( // TODO(Rugvip): From the old GitlabReaderProcessor; used // the existence of /-/blob/ to switch the logic. Don't know if this // makes sense and it might require some more work. + if (url.includes('/-/blob/')) { const projectID = await getProjectId(url, config); - return buildProjectUrl(url, projectID).toString(); + return buildProjectUrl(url, projectID, config).toString(); } return buildRawUrl(url).toString(); } @@ -101,20 +105,27 @@ export function buildRawUrl(target: string): URL { // Converts // from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath // to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch -export function buildProjectUrl(target: string, projectID: Number): URL { +export function buildProjectUrl( + target: string, + projectID: Number, + config: GitLabIntegrationConfig, +): URL { try { const url = new URL(target); const branchAndFilePath = url.pathname.split('/-/blob/')[1]; const [branch, ...filePath] = branchAndFilePath.split('/'); + const relativePath = getGitLabIntegrationRelativePath(config); url.pathname = [ - '/api/v4/projects', + ...(relativePath ? [relativePath] : []), + 'api/v4/projects', projectID, 'repository/files', encodeURIComponent(decodeURIComponent(filePath.join('/'))), 'raw', ].join('/'); + url.search = `?ref=${branch}`; return url; @@ -137,19 +148,29 @@ export async function getProjectId( } try { - const repo = url.pathname.split('/-/blob/')[0]; + let repo = url.pathname.split('/-/blob/')[0]; + + // Get gitlab relative path + const relativePath = getGitLabIntegrationRelativePath(config); + + // Check relative path exist and replace it if it's the case. + if (relativePath) { + repo = repo.replace(relativePath, ''); + } // Convert // to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo const repoIDLookup = new URL( - `${url.origin}/api/v4/projects/${encodeURIComponent( + `${url.origin}${relativePath}/api/v4/projects/${encodeURIComponent( repo.replace(/^\//, ''), )}`, ); + const response = await fetch( repoIDLookup.toString(), getGitLabRequestOptions(config), ); + const data = await response.json(); if (!response.ok) { diff --git a/packages/integration/src/gitlab/index.ts b/packages/integration/src/gitlab/index.ts index e8d6665a6f..1a5063c51c 100644 --- a/packages/integration/src/gitlab/index.ts +++ b/packages/integration/src/gitlab/index.ts @@ -17,6 +17,7 @@ export { readGitLabIntegrationConfig, readGitLabIntegrationConfigs, + getGitLabIntegrationRelativePath, } from './config'; export type { GitLabIntegrationConfig } from './config'; export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core';