chore(scaffolder): refactoring some of the entity parsing away so that we can re-use it. and fix some of the semantic namings

This commit is contained in:
blam
2020-06-18 17:42:38 +02:00
parent ca1d5fa418
commit 299609a020
6 changed files with 121 additions and 55 deletions
@@ -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<string> {
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;
@@ -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,
};
};
@@ -15,3 +15,4 @@
*/
export * from './preparers';
export * from './types';
export * from './helpers';
@@ -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');
});
@@ -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<RemoteLocation, PreparerBase>();
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
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;
}
}
@@ -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';