refactor to a isolated module

Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
Rogerio Angeliski
2021-06-24 12:00:43 -03:00
parent aad73cf398
commit c5e81eab43
23 changed files with 423 additions and 56 deletions
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,225 @@
# scaffolder-backend-module-rails
Welcome to the Rails Module for Scaffolder.
Here you can find all Rails related features to improve your scaffolder:
- Rails Action to use the `new` command
- More features are coming
## Getting started
You need to configure the action in your backend:
```
yarn add @backstage/plugin-scaffolder-backend-module-rails
```
Configure the action (you can check
the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to
see all options):
```typescript
const actions = [
createFetchRailsAction({
integrations,
reader,
containerRunner,
}),
];
return await createRouter({
preparers,
templaters,
publishers,
logger,
config,
database,
catalogClient,
reader,
actions,
});
```
After that you can use the action in your template:
```yaml
apiVersion: backstage.io/v1beta2
kind: Template
metadata:
name: rails-demo
title: Rails template
description: scaffolder Rails app
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: Fill in some steps
required:
- name
- owner
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
defaultKind: System
- title: Choose a location
required:
- repoUrl
- dryRun
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
dryRun:
title: Only perform a dry run, don't publish anything
type: boolean
default: false
railsArguments:
title: arguments to run the rails new command
type: object
properties:
minimal:
title: minimal
description: Preconfigure a minimal rails app
type: boolean
skipBundle:
title: skipBundle
description: Don't run bundle install
type: boolean
skipWebpackInstall:
title: skipWebpackInstall
description: Don't run Webpack install
type: boolean
api:
title: api
description: Preconfigure smaller stack for API only apps
type: boolean
template:
title: template
description: Path to some application template (can be a filesystem path or URL)
type: string
default: './rails-template-file.rb'
webpacker:
title: webpacker
description:
'Preconfigure Webpack with a particular framework (options: react,
vue, angular, elm, stimulus)'
type: string
enum:
- react
- vue
- angular
- elm
- stimulus
database:
title: database
description: 'Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)'
type: string
enum:
- mysql
- postgresql
- sqlite3
- oracle
- sqlserver
- jdbcmysql
- jdbcsqlite3
- jdbcpostgresql
- jdbc
railsVersion:
title: Rails version in Gemfile
description:
'Set up the application with Gemfile pointing to a specific version
(options: dev, edge, master)'
type: string
enum:
- dev
- edge
- master
steps:
- id: fetch-base
name: Fetch Base
action: fetch:rails
input:
url: ./template
values:
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
system: '{{ parameters.system }}'
railsArguments: '{{ json parameters.railsArguments }}'
- name: Write Catalog information
action: catalog:write
input:
component:
apiVersion: 'backstage.io/v1alpha1'
kind: Component
metadata:
name: '{{ parameters.name }}'
annotations:
github.com/project-slug: '{{ projectSlug parameters.repoUrl }}'
spec:
type: service
lifecycle: production
owner: '{{ parameters.owner }}'
- id: publish
if: '{{ not parameters.dryRun }}'
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: 'This is {{ parameters.name }}'
repoUrl: '{{ parameters.repoUrl }}'
- id: register
if: '{{ not parameters.dryRun }}'
name: Register
action: catalog:register
input:
repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
catalogInfoPath: '/catalog-info.yaml'
- name: Results
if: '{{ parameters.dryRun }}'
action: debug:log
input:
listWorkspace: true
output:
links:
- title: Repository
url: '{{ steps.publish.output.remoteUrl }}'
- title: Open in catalog
icon: 'catalog'
entityRef: '{{ steps.register.output.entityRef }}'
```
We have a [Docker image](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/Rails.dockerfile) you can use to build your own.
@@ -0,0 +1,22 @@
## API Report File for "@backstage/plugin-scaffolder-backend-module-rails"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ContainerRunner } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-backend';
import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
export function createFetchRailsAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
containerRunner: ContainerRunner;
}): TemplateAction<any>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,44 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-rails",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.8.3",
"@backstage/plugin-scaffolder-backend": "^0.12.2",
"@backstage/config": "^0.1.5",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.6",
"command-exists": "^1.2.9",
"fs-extra": "^9.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.7.1",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/command-exists": "^1.2.0",
"@types/fs-extra": "^9.0.1",
"@types/mock-fs": "^4.13.0",
"jest-when": "^3.1.0",
"mock-fs": "^4.13.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
export * from './rails';
@@ -15,24 +15,31 @@
*/
const mockRailsTemplater = { run: jest.fn() };
jest.mock('./helpers');
jest.mock('../../../stages/templater', () => {
jest.mock('@backstage/plugin-scaffolder-backend', () => ({
...jest.requireActual('@backstage/plugin-scaffolder-backend'),
fetchContents: jest.fn(),
}));
jest.mock('./railsNewRunner', () => {
return {
Rails: jest.fn().mockImplementation(() => {
RailsNewRunner: jest.fn().mockImplementation(() => {
return mockRailsTemplater;
}),
};
});
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
import {
ContainerRunner,
getVoidLogger,
UrlReader,
} from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import mock from 'mock-fs';
import os from 'os';
import { resolve as resolvePath } from 'path';
import { PassThrough } from 'stream';
import { createFetchRailsAction } from './rails';
import { fetchContents } from './helpers';
import { createFetchRailsAction } from './index';
import { fetchContents } from '@backstage/plugin-scaffolder-backend';
describe('fetch:rails', () => {
const integrations = ScmIntegrations.fromConfig(
@@ -68,10 +75,14 @@ describe('fetch:rails', () => {
readTree: jest.fn(),
search: jest.fn(),
};
const containerRunner: ContainerRunner = {
runContainer: jest.fn(),
};
const action = createFetchRailsAction({
integrations,
reader: mockReader,
containerRunner,
});
beforeEach(() => {
@@ -14,22 +14,25 @@
* limitations under the License.
*/
import { DockerContainerRunner, UrlReader } from '@backstage/backend-common';
import { ContainerRunner, UrlReader } from '@backstage/backend-common';
import { JsonObject } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import fs from 'fs-extra';
import {
createTemplateAction,
fetchContents,
} from '@backstage/plugin-scaffolder-backend';
import { resolve as resolvePath } from 'path';
import { Rails, TemplaterValues } from '../../../stages/templater';
import { createTemplateAction } from '../../createTemplateAction';
import { fetchContents } from './helpers';
import Docker from 'dockerode';
import { RailsNewRunner } from './railsNewRunner';
export function createFetchRailsAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
containerRunner: ContainerRunner;
}) {
const { reader, integrations } = options;
const { reader, integrations, containerRunner } = options;
return createTemplateAction<{
url: string;
@@ -39,7 +42,7 @@ export function createFetchRailsAction(options: {
}>({
id: 'fetch:rails',
description:
'Downloads a template from the given URL into the workspace, and runs a rails generator on it.',
'Downloads a template from the given URL into the workspace, and runs a rails new generator on it.',
schema: {
input: {
type: 'object',
@@ -152,12 +155,10 @@ export function createFetchRailsAction(options: {
outputPath: workDir,
});
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const templateRunner = new Rails({ containerRunner });
const templateRunner = new RailsNewRunner({ containerRunner });
const values = {
...(ctx.input.values as TemplaterValues),
...ctx.input.values,
imageName: ctx.input.imageName,
};
@@ -15,7 +15,6 @@
*/
import { railsArgumentResolver } from './railsArgumentResolver';
import { TemplaterValues } from '../types';
describe('railsArgumentResolver', () => {
describe('when provide the parameter', () => {
@@ -33,7 +32,7 @@ describe('railsArgumentResolver', () => {
'should include the argument to execution %p -> %p',
(passedArguments: object, expected: Array<string>) => {
// that step is to ensure the validation between the TemplaterValues and the resolver
const values: TemplaterValues = {
const values = {
owner: 'r',
storePath: '',
railsArguments: passedArguments,
@@ -16,7 +16,7 @@
const runCommand = jest.fn();
const commandExists = jest.fn();
jest.mock('../helpers', () => ({ runCommand }));
jest.mock('@backstage/plugin-scaffolder-backend', () => ({ runCommand }));
jest.mock('command-exists', () => commandExists);
jest.mock('fs-extra');
@@ -26,7 +26,7 @@ import fs from 'fs-extra';
import path from 'path';
import { PassThrough } from 'stream';
import { Rails } from './index';
import { RailsNewRunner } from './railsNewRunner';
describe('Rails Templater', () => {
const containerRunner: jest.Mocked<ContainerRunner> = {
@@ -39,6 +39,7 @@ describe('Rails Templater', () => {
describe('when running on docker', () => {
it('should run the correct bindings for the volumes', async () => {
const logStream = new PassThrough();
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
@@ -51,10 +52,11 @@ describe('Rails Templater', () => {
.spyOn(fs, 'realpath')
.mockImplementation(x => Promise.resolve(x.toString()));
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith({
@@ -67,11 +69,12 @@ describe('Rails Templater', () => {
[path.join('tempdir', 'intermediate')]: '/output',
},
workingDir: '/input',
logStream: undefined,
logStream: logStream,
});
});
it('should use the provided imageName', async () => {
const logStream = new PassThrough();
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
@@ -81,10 +84,11 @@ describe('Rails Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith(
@@ -106,7 +110,7 @@ describe('Rails Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
@@ -128,6 +132,7 @@ describe('Rails Templater', () => {
});
it('update the template path to correct location', async () => {
const logStream = new PassThrough();
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
@@ -141,10 +146,11 @@ describe('Rails Templater', () => {
.spyOn(fs, 'realpath')
.mockImplementation(x => Promise.resolve(x.toString()));
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith({
@@ -162,7 +168,7 @@ describe('Rails Templater', () => {
[path.join('tempdir', 'intermediate')]: '/output',
},
workingDir: '/input',
logStream: undefined,
logStream: logStream,
});
});
});
@@ -181,7 +187,7 @@ describe('Rails Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
commandExists.mockImplementationOnce(() => () => true);
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
@@ -211,7 +217,7 @@ describe('Rails Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
commandExists.mockImplementationOnce(() => () => true);
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
@@ -239,7 +245,7 @@ describe('Rails Templater', () => {
.spyOn(fs, 'readdir')
.mockImplementationOnce(() => Promise.resolve([]));
const templater = new Rails({ containerRunner });
const templater = new RailsNewRunner({ containerRunner });
await expect(
templater.run({
workspacePath: 'tempdir',
@@ -17,12 +17,16 @@
import { ContainerRunner } from '@backstage/backend-common';
import fs from 'fs-extra';
import path from 'path';
import { runCommand } from '../helpers';
import { runCommand } from '@backstage/plugin-scaffolder-backend';
import commandExists from 'command-exists';
import { TemplaterBase, TemplaterRunOptions } from '../types';
import { railsArgumentResolver } from './railsArgumentResolver';
import {
railsArgumentResolver,
RailsRunOptions,
} from './railsArgumentResolver';
import { JsonObject } from '@backstage/config';
import { Writable } from 'stream';
export class Rails implements TemplaterBase {
export class RailsNewRunner {
private readonly containerRunner: ContainerRunner;
constructor({ containerRunner }: { containerRunner: ContainerRunner }) {
@@ -33,7 +37,11 @@ export class Rails implements TemplaterBase {
workspacePath,
values,
logStream,
}: TemplaterRunOptions): Promise<void> {
}: {
workspacePath: string;
values: JsonObject;
logStream: Writable;
}): Promise<void> {
const intermediateDir = path.join(workspacePath, 'intermediate');
await fs.ensureDir(intermediateDir);
const resultDir = path.join(workspacePath, 'result');
@@ -53,7 +61,7 @@ export class Rails implements TemplaterBase {
if (commandExistsToRun) {
const arrayExtraArguments = railsArgumentResolver(
workspacePath,
railsArguments,
railsArguments as RailsRunOptions,
);
await runCommand({
@@ -68,10 +76,10 @@ export class Rails implements TemplaterBase {
} else {
const arrayExtraArguments = railsArgumentResolver(
'/input',
railsArguments,
railsArguments as RailsRunOptions,
);
await this.containerRunner.runContainer({
imageName: imageName,
imageName: imageName as string,
command: baseCommand,
args: [...baseArguments, `/output/${name}`, ...arrayExtraArguments],
mountDirs,
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
export * from './fetch';
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
export * from './actions';
+3 -3
View File
@@ -11,7 +11,7 @@ import { createPullRequest } from 'octokit-plugin-create-pull-request';
import express from 'express';
import { JsonObject } from '@backstage/config';
import { JsonValue } from '@backstage/config';
import { Logger } from 'winston';
import { Logger as Logger_2 } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { Schema } from 'jsonschema';
import { ScmIntegrationRegistry } from '@backstage/integration';
@@ -23,7 +23,7 @@ import { Writable } from 'stream';
// @public (undocumented)
export type ActionContext<Input extends InputBase> = {
baseUrl?: string;
logger: Logger;
logger: Logger_2;
logStream: Writable;
token?: string | undefined;
workspacePath: string;
@@ -131,7 +131,7 @@ export interface RouterOptions {
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
logger: Logger;
logger: Logger_2;
// (undocumented)
reader: UrlReader;
// (undocumented)
+1 -3
View File
@@ -62,8 +62,7 @@
"octokit-plugin-create-pull-request": "^3.9.3",
"uuid": "^8.2.0",
"winston": "^3.2.1",
"yaml": "^1.10.0",
"dockerode": "^3.2.1"
"yaml": "^1.10.0"
},
"devDependencies": {
"@backstage/cli": "^0.7.3",
@@ -73,7 +72,6 @@
"@types/git-url-parse": "^9.0.0",
"@types/mock-fs": "^4.13.0",
"@types/supertest": "^2.0.8",
"@types/dockerode": "^3.2.1",
"jest-when": "^3.1.0",
"mock-fs": "^4.13.0",
"msw": "^0.29.0",
@@ -24,11 +24,7 @@ import {
} from './catalog';
import { createDebugLogAction } from './debug';
import {
createFetchCookiecutterAction,
createFetchPlainAction,
createFetchRailsAction,
} from './fetch';
import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch';
import {
createFilesystemDeleteAction,
createFilesystemRenameAction,
@@ -66,10 +62,6 @@ export const createBuiltinActions = (options: {
integrations,
containerRunner,
}),
createFetchRailsAction({
reader,
integrations,
}),
createPublishGithubAction({
integrations,
config,
@@ -16,4 +16,4 @@
export { createFetchPlainAction } from './plain';
export { createFetchCookiecutterAction } from './cookiecutter';
export { createFetchRailsAction } from './rails';
export * from './helpers';
@@ -18,4 +18,3 @@ export * from './types';
export * from './helpers';
export * from './templaters';
export * from './cra';
export * from './rails';