From 299609a0207ab23402c9421f5b065f380a22a325 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 18 Jun 2020 17:42:38 +0200 Subject: [PATCH] chore(scaffolder): refactoring some of the entity parsing away so that we can re-use it. and fix some of the semantic namings --- .../src/scaffolder/prepare/file.ts | 26 +++------ .../src/scaffolder/prepare/helpers.ts | 56 +++++++++++++++++++ .../src/scaffolder/prepare/index.ts | 1 + .../src/scaffolder/prepare/preparers.test.ts | 54 +++++++++++++++--- .../src/scaffolder/prepare/preparers.ts | 33 +++-------- .../src/scaffolder/prepare/types.ts | 6 +- 6 files changed, 121 insertions(+), 55 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/prepare/helpers.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/prepare/file.ts index b9e8942aae..7a924d9a99 100644 --- a/plugins/scaffolder-backend/src/scaffolder/prepare/file.ts +++ b/plugins/scaffolder-backend/src/scaffolder/prepare/file.ts @@ -16,32 +16,20 @@ import fs from 'fs-extra'; import path from 'path'; import os from 'os'; -import { - TemplateEntityV1alpha1, - LOCATION_ANNOTATION, -} from '@backstage/catalog-model'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { parseLocationAnnotation } from './helpers'; import { InputError } from '@backstage/backend-common'; import { PreparerBase } from './types'; export class FilePreparer implements PreparerBase { async prepare(template: TemplateEntityV1alpha1): Promise { - const location = template?.metadata?.annotations?.[LOCATION_ANNOTATION]; + const { protocol, location } = parseLocationAnnotation(template); - const [locationType, templateEntityLocation] = (location ?? '').split( - /:(.+)/, - ); - if (locationType !== 'file') { + if (protocol !== 'file') { throw new InputError( - `Wrong location type: ${locationType}, should be 'file'`, + `Wrong location protocol: ${protocol}, should be 'file'`, ); } - - if (!templateEntityLocation) { - throw new InputError( - `Couldn't parse location for template: ${template.metadata.name}`, - ); - } - const templateId = template.metadata.name; const tempDir = await fs.promises.mkdtemp( @@ -49,12 +37,12 @@ export class FilePreparer implements PreparerBase { ); const parentDirectory = path.resolve( - path.dirname(templateEntityLocation), + path.dirname(location), template.spec.path ?? '.', ); await fs.copy(parentDirectory, tempDir, { - filter: src => src !== templateEntityLocation, + filter: src => src !== location, }); return tempDir; diff --git a/plugins/scaffolder-backend/src/scaffolder/prepare/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/prepare/helpers.ts new file mode 100644 index 0000000000..3f31f19d59 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/prepare/helpers.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + TemplateEntityV1alpha1, + LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; +import { InputError } from '@backstage/backend-common'; +import { RemoteProtocol } from './types'; + +export type ParsedLocationAnnotation = { + protocol: RemoteProtocol; + location: string; +}; + +export const parseLocationAnnotation = ( + entity: TemplateEntityV1alpha1, +): ParsedLocationAnnotation => { + const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + + if (!annotation) { + throw new InputError( + `No location annotation provided in entity: ${entity.metadata.name}`, + ); + } + + // split on the first colon for the protocol and the rest after the first split + // is the location. + const [protocol, location] = annotation.split(/:(.+)/) as [ + RemoteProtocol?, + string?, + ]; + + if (!protocol || !location) { + throw new InputError( + `Failure to parse either protocol or location for entity: ${entity.metadata.name}`, + ); + } + + return { + protocol, + location, + }; +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/prepare/index.ts b/plugins/scaffolder-backend/src/scaffolder/prepare/index.ts index 748e5cebc7..b9bc3a4294 100644 --- a/plugins/scaffolder-backend/src/scaffolder/prepare/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/prepare/index.ts @@ -15,3 +15,4 @@ */ export * from './preparers'; export * from './types'; +export * from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.test.ts index 96e6b7d5b9..a8f4b4ae99 100644 --- a/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.test.ts @@ -15,18 +15,54 @@ */ import { Preparers } from '.'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { FilePreparer } from './file'; describe('Preparers', () => { + const mockTemplate: TemplateEntityV1alpha1 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Template', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'file:/Users/blam/dev/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', + }, + name: 'react-ssr-template', + title: 'React SSR Template', + description: + 'Next.js application skeleton for creating isomorphic web applications.', + uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', + etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', + generation: 1, + }, + spec: { + type: 'cookiecutter', + path: '.', + }, + }; it('should throw an error when the preparer for the source location is not registered', () => { const preparers = new Preparers(); - const mockTemplate: TemplateEntityV1alpha1 = { + + expect(() => preparers.get(mockTemplate)).toThrow( + expect.objectContaining({ + message: 'No preparer registered for type: "file"', + }), + ); + }); + it('should return the correct preparer when the source matches', () => { + const preparers = new Preparers(); + const preparer = new FilePreparer(); + + preparers.register('file', preparer); + + expect(preparers.get(mockTemplate)).toBe(preparer); + }); + + it('should throw an error if the metadata tag does not exist in the entity', () => { + const brokenTemplate: TemplateEntityV1alpha1 = { apiVersion: 'backstage.io/v1alpha1', kind: 'Template', metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'file:/Users/blam/dev/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', - }, + annotations: {}, name: 'react-ssr-template', title: 'React SSR Template', description: @@ -41,12 +77,12 @@ describe('Preparers', () => { }, }; - expect(() => preparers.get(mockTemplate)).toThrow( + const preparers = new Preparers(); + + expect(() => preparers.get(brokenTemplate)).toThrow( expect.objectContaining({ - message: 'No preparer registered for type file', + message: expect.stringContaining('No location annotation provided'), }), ); }); - it('should return the correct preparer when the source matches'); - it('should throw an error if the srouce is not available'); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.ts index 97db3a3100..046890bdca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/prepare/preparers.ts @@ -14,40 +14,25 @@ * limitations under the License. */ -import { PreparerBase, RemoteLocation, PreparerBuilder } from './types'; -import { - TemplateEntityV1alpha1, - LOCATION_ANNOTATION, -} from '@backstage/catalog-model'; +import { PreparerBase, RemoteProtocol, PreparerBuilder } from './types'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { parseLocationAnnotation } from './helpers'; export class Preparers implements PreparerBuilder { - private preparerMap = new Map(); + private preparerMap = new Map(); - register(key: RemoteLocation, processor: PreparerBase) { - this.preparerMap.set(key, processor); + register(protocol: RemoteProtocol, preparer: PreparerBase) { + this.preparerMap.set(protocol, preparer); } get(template: TemplateEntityV1alpha1): PreparerBase { - const preparerKey = this.getPreparerKeyFromEntity(template); - const preparer = this.preparerMap.get(preparerKey); + const { protocol } = parseLocationAnnotation(template); + const preparer = this.preparerMap.get(protocol); if (!preparer) { - throw new Error(`No preparer registered for type ${preparerKey}`); + throw new Error(`No preparer registered for type: "${protocol}"`); } return preparer; } - - private getPreparerKeyFromEntity( - entity: TemplateEntityV1alpha1, - ): RemoteLocation { - const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION] ?? ''; - const [key] = annotation?.split(':'); - - if (!key) { - throw new Error('Failed to parse the location data'); - } - - return key as RemoteLocation; - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/prepare/types.ts index 1c703d10fd..3251bd2780 100644 --- a/plugins/scaffolder-backend/src/scaffolder/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/prepare/types.ts @@ -25,8 +25,8 @@ export type PreparerBase = { }; export type PreparerBuilder = { - register(key: RemoteLocation, preparer: PreparerBase): void; - get(key: TemplateEntityV1alpha1): PreparerBase; + register(protocol: RemoteProtocol, preparer: PreparerBase): void; + get(template: TemplateEntityV1alpha1): PreparerBase; }; -export type RemoteLocation = 'file'; +export type RemoteProtocol = 'file';