UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

306 lines (300 loc) • 12.1 kB
'use strict'; var fetch = require('node-fetch'); var errors = require('@backstage/errors'); var integration = require('@backstage/integration'); var parseGitUrl = require('git-url-parse'); var lodash = require('lodash'); var minimatch = require('minimatch'); var stream = require('stream'); var ReadUrlResponseFactory = require('./ReadUrlResponseFactory.cjs.js'); var util = require('./util.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch); var parseGitUrl__default = /*#__PURE__*/_interopDefaultCompat(parseGitUrl); class GitlabUrlReader { static factory = ({ config, treeResponseFactory }) => { const integrations = integration.ScmIntegrations.fromConfig(config); return integrations.gitlab.list().map((integration) => { const reader = new GitlabUrlReader(integration, { treeResponseFactory }); const predicate = (url) => url.host === integration.config.host; return { reader, predicate }; }); }; integration; deps; constructor(integration, deps) { this.integration = integration; this.deps = deps; } async read(url) { const response = await this.readUrl(url); return response.buffer(); } async readUrl(url, options) { const { etag, lastModifiedAfter, signal, token } = options ?? {}; const isArtifact = url.includes("/-/jobs/artifacts/"); const builtUrl = await this.getGitlabFetchUrl(url, token); let response; try { response = await fetch__default.default(builtUrl, { headers: { ...integration.getGitLabRequestOptions(this.integration.config, token).headers, ...etag && !isArtifact && { "If-None-Match": etag }, ...lastModifiedAfter && !isArtifact && { "If-Modified-Since": lastModifiedAfter.toUTCString() } }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can be // removed after we support ESM for CLI dependencies and migrate to // version 3 of node-fetch. // https://github.com/backstage/backstage/issues/8242 ...signal && { signal } }); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } if (response.status === 304) { throw new errors.NotModifiedError(); } if (response.ok) { return ReadUrlResponseFactory.ReadUrlResponseFactory.fromNodeJSReadable(response.body, { etag: response.headers.get("ETag") ?? void 0, lastModifiedAt: util.parseLastModified( response.headers.get("Last-Modified") ) }); } const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; if (response.status === 404) { throw new errors.NotFoundError(message); } throw new Error(message); } async readTree(url, options) { const { etag, signal, token } = options ?? {}; const { ref, full_name, filepath } = parseGitUrl__default.default(url); let repoFullName = full_name; const relativePath = integration.getGitLabIntegrationRelativePath( this.integration.config ); if (relativePath) { const rectifiedRelativePath = `${lodash.trimStart(relativePath, "/")}/`; repoFullName = full_name.replace(rectifiedRelativePath, ""); } const projectGitlabResponse = await fetch__default.default( new URL( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( repoFullName )}` ).toString(), integration.getGitLabRequestOptions(this.integration.config, token) ); if (!projectGitlabResponse.ok) { const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`; if (projectGitlabResponse.status === 404) { throw new errors.NotFoundError(msg); } throw new Error(msg); } const projectGitlabResponseJson = await projectGitlabResponse.json(); const branch = ref || projectGitlabResponseJson.default_branch; const commitsReqParams = new URLSearchParams(); commitsReqParams.set("ref_name", branch); if (!!filepath) { commitsReqParams.set("path", filepath); } const commitsGitlabResponse = await fetch__default.default( new URL( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( repoFullName )}/repository/commits?${commitsReqParams.toString()}` ).toString(), { ...integration.getGitLabRequestOptions(this.integration.config, token), // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can // be removed after we support ESM for CLI dependencies and migrate to // version 3 of node-fetch. // https://github.com/backstage/backstage/issues/8242 ...signal && { signal } } ); if (!commitsGitlabResponse.ok) { const message = `Failed to read tree (branch) from ${url}, ${commitsGitlabResponse.status} ${commitsGitlabResponse.statusText}`; if (commitsGitlabResponse.status === 404) { throw new errors.NotFoundError(message); } throw new Error(message); } const commitSha = (await commitsGitlabResponse.json())[0]?.id ?? ""; if (etag && etag === commitSha) { throw new errors.NotModifiedError(); } const archiveReqParams = new URLSearchParams(); archiveReqParams.set("sha", branch); if (!!filepath) { archiveReqParams.set("path", filepath); } const archiveGitLabResponse = await fetch__default.default( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( repoFullName )}/repository/archive?${archiveReqParams.toString()}`, { ...integration.getGitLabRequestOptions(this.integration.config, token), // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can // be removed after we support ESM for CLI dependencies and migrate to // version 3 of node-fetch. // https://github.com/backstage/backstage/issues/8242 ...signal && { signal } } ); if (!archiveGitLabResponse.ok) { const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; if (archiveGitLabResponse.status === 404) { throw new errors.NotFoundError(message); } throw new Error(message); } return await this.deps.treeResponseFactory.fromTarArchive({ stream: stream.Readable.from(archiveGitLabResponse.body), subpath: filepath, etag: commitSha, filter: options?.filter }); } async search(url, options) { const { filepath } = parseGitUrl__default.default(url); if (!filepath?.match(/[*?]/)) { try { const data = await this.readUrl(url, options); return { files: [ { url, content: data.buffer, lastModifiedAt: data.lastModifiedAt } ], etag: data.etag ?? "" }; } catch (error) { errors.assertError(error); if (error.name === "NotFoundError") { return { files: [], etag: "" }; } throw error; } } const staticPart = this.getStaticPart(filepath); const matcher = new minimatch.Minimatch(filepath); const treeUrl = lodash.trimEnd(url.replace(filepath, staticPart), `/`); const pathPrefix = staticPart ? `${staticPart}/` : ""; const tree = await this.readTree(treeUrl, { etag: options?.etag, signal: options?.signal, filter: (path) => matcher.match(`${pathPrefix}${path}`) }); const files = await tree.files(); return { etag: tree.etag, files: files.map((file) => ({ url: this.integration.resolveUrl({ url: `/${pathPrefix}${file.path}`, base: url }), content: file.content, lastModifiedAt: file.lastModifiedAt })) }; } /** * This function splits the input globPattern string into segments using the path separator /. It then iterates over * the segments from the end of the array towards the beginning, checking if the concatenated string up to that * segment matches the original globPattern using the minimatch function. If a match is found, it continues iterating. * If no match is found, it returns the concatenated string up to the current segment, which is the static part of the * glob pattern. * * E.g. `catalog/foo/*.yaml` will return `catalog/foo`. * * @param globPattern - the glob pattern */ getStaticPart(globPattern) { const segments = globPattern.split("/"); const globIndex = segments.findIndex((segment) => segment.match(/[*?]/)); return globIndex === -1 ? globPattern : segments.slice(0, globIndex).join("/"); } toString() { const { host, token } = this.integration.config; return `gitlab{host=${host},authed=${Boolean(token)}}`; } async getGitlabFetchUrl(target, token) { const targetUrl = new URL(target); if (targetUrl.pathname.includes("/-/jobs/artifacts/")) { return this.getGitlabArtifactFetchUrl(targetUrl, token).then( (value) => value.toString() ); } return integration.getGitLabFileFetchUrl(target, this.integration.config, token); } // convert urls of the form: // https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/raw/<path_to_file>?job=<job_name> // to urls of the form: // https://example.com/api/v4/projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=<job_name> async getGitlabArtifactFetchUrl(target, token) { if (!target.pathname.includes("/-/jobs/artifacts/")) { throw new Error("Unable to process url as an GitLab artifact"); } try { const [namespaceAndProject, ref] = target.pathname.split("/-/jobs/artifacts/"); const projectPath = new URL(target); projectPath.pathname = namespaceAndProject; const projectId = await this.resolveProjectToId(projectPath, token); const relativePath = integration.getGitLabIntegrationRelativePath( this.integration.config ); const newUrl = new URL(target); newUrl.pathname = `${relativePath}/api/v4/projects/${projectId}/jobs/artifacts/${ref}`; return newUrl; } catch (e) { throw new Error( `Unable to translate GitLab artifact URL: ${target}, ${e}` ); } } async resolveProjectToId(pathToProject, token) { let project = pathToProject.pathname; const relativePath = integration.getGitLabIntegrationRelativePath( this.integration.config ); if (relativePath) { project = project.replace(relativePath, ""); } project = project.replace(/^\//, ""); const result = await fetch__default.default( `${pathToProject.origin}${relativePath}/api/v4/projects/${encodeURIComponent(project)}`, integration.getGitLabRequestOptions(this.integration.config, token) ); const data = await result.json(); if (!result.ok) { if (result.status === 401) { throw new Error( "GitLab Error: 401 - Unauthorized. The access token used is either expired, or does not have permission to read the project" ); } throw new Error(`Gitlab error: ${data.error}, ${data.error_description}`); } return Number(data.id); } } exports.GitlabUrlReader = GitlabUrlReader; //# sourceMappingURL=GitlabUrlReader.cjs.js.map