diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/builtin.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/builtin.ts index 60e3ad5282..1427d06880 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/builtin.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/builtin.ts @@ -21,6 +21,100 @@ import { TemplaterBuilder, TemplaterValues } from '../stages/templater'; import { TemplateAction } from './types'; import { InputError, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; +import { JsonValue } from '@backstage/config'; + +async function fetchContents({ + urlReader, + integrations, + baseUrl, + fetchUrl = '.', + outputPath, +}: { + urlReader: UrlReader; + integrations: ScmIntegrations; + baseUrl?: string; + fetchUrl?: JsonValue; + outputPath: string; +}) { + if (typeof fetchUrl !== 'string') { + throw new InputError( + `Invalid url parameter, expected string, got ${typeof fetchUrl}`, + ); + } + + // We handle both file locations and url ones + if (baseUrl?.startsWith('file://')) { + const basePath = baseUrl.slice('file://'.length); + if (isAbsolute(fetchUrl)) { + throw new InputError( + `Fetch URL may not be absolute for file locations, ${fetchUrl}`, + ); + } + const srcDir = resolvePath(basePath, '..', fetchUrl); + await fs.copy(srcDir, outputPath); + } else { + let readUrl; + + if (baseUrl) { + const integration = integrations.byUrl(baseUrl); + if (!integration) { + throw new InputError(`No integration found for location ${baseUrl}`); + } + readUrl = integration.resolveUrl({ + url: fetchUrl, + base: baseUrl, + }); + } else { + // If we don't have a baseUrl, check if our provided fetch url is absolute + try { + readUrl = new URL(fetchUrl).toString(); + } catch { + throw new InputError( + `Failed to fetch, template location could not be determined and the fetch URL is relative, ${fetchUrl}`, + ); + } + + const res = await urlReader.readTree(readUrl); + await res.dir({ targetDir: outputPath }); + } + } +} + +export function createFetchPlainAction(options: { + urlReader: UrlReader; + integrations: ScmIntegrations; +}): TemplateAction { + const { urlReader, integrations } = options; + + return { + id: 'fetch:plain', + async handler(ctx) { + ctx.logger.info('Fetching plain content from remote URL'); + + // Finally move the template result into the task workspace + const targetPath = ctx.parameters.targetPath ?? './'; + if (typeof targetPath !== 'string') { + throw new InputError( + `Fetch action targetPath is not a string, got ${targetPath}`, + ); + } + const outputPath = resolvePath(ctx.workspacePath, targetPath); + if (!outputPath.startsWith(ctx.workspacePath)) { + throw new InputError( + `Fetch action targetPath may not specify a path outside the working directory`, + ); + } + + await fetchContents({ + urlReader, + integrations, + baseUrl: ctx.baseUrl, + fetchUrl: ctx.parameters.url, + outputPath, + }); + }, + }; +} export function createFetchCookiecutterAction(options: { dockerClient: Docker; @@ -42,51 +136,13 @@ export function createFetchCookiecutterAction(options: { ); const resultDir = resolvePath(workDir, 'result'); - const fetchUrl = ctx.parameters.url ?? '.'; - if (typeof fetchUrl !== 'string') { - throw new InputError( - `Invalid url parameter, expected string, got ${typeof fetchUrl}`, - ); - } - - // We handle both file locations and url ones - if (ctx.baseUrl?.startsWith('file://')) { - const basePath = ctx.baseUrl.slice('file://'.length); - if (isAbsolute(fetchUrl)) { - throw new InputError( - `Fetch URL may not be absolute for file locations, ${fetchUrl}`, - ); - } - const srcDir = resolvePath(basePath, '..', fetchUrl); - await fs.copy(srcDir, templateContentsDir); - } else { - let readUrl; - - if (ctx.baseUrl) { - const integration = integrations.byUrl(ctx.baseUrl); - if (!integration) { - throw new InputError( - `No integration found for location ${ctx.baseUrl}`, - ); - } - readUrl = integration.resolveUrl({ - url: fetchUrl, - base: ctx.baseUrl, - }); - } else { - // If we don't have a baseUrl, check if our provided fetch url is absolute - try { - readUrl = new URL(fetchUrl).toString(); - } catch { - throw new InputError( - `Failed to fetch, template location could not be determined and the fetch URL is relative, ${fetchUrl}`, - ); - } - - const res = await urlReader.readTree(readUrl); - await res.dir({ targetDir: templateContentsDir }); - } - } + await fetchContents({ + urlReader, + integrations, + baseUrl: ctx.baseUrl, + fetchUrl: ctx.parameters.url, + outputPath: templateContentsDir, + }); const cookiecutter = templaters.get('cookiecutter'); if (!cookiecutter) { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 3787d916b5..370924024b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -54,7 +54,10 @@ import { TemplateEntityV1beta2, Entity, } from '@backstage/catalog-model'; -import { createFetchCookiecutterAction } from '../scaffolder/tasks/builtin'; +import { + createFetchPlainAction, + createFetchCookiecutterAction, +} from '../scaffolder/tasks/builtin'; import { ScmIntegrations } from '@backstage/integration'; export interface RouterOptions { @@ -128,6 +131,12 @@ export async function createRouter( templaters, catalogClient, }); + actionRegistry.register( + createFetchPlainAction({ + urlReader, + integrations, + }), + ); actionRegistry.register( createFetchCookiecutterAction({ urlReader,