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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user