feat: added rails templater to scaffolder-backend
Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Created a Rails templater to scaffold apps
|
||||
@@ -40,6 +40,9 @@
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@types/express": "^4.17.6",
|
||||
"azure-devops-node-api": "^10.2.2",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"azure-devops-node-api": "^10.1.1",
|
||||
"command-exists": "^1.2.9",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
@@ -60,7 +63,8 @@
|
||||
"octokit-plugin-create-pull-request": "^3.9.3",
|
||||
"uuid": "^8.2.0",
|
||||
"winston": "^3.2.1",
|
||||
"yaml": "^1.10.0"
|
||||
"yaml": "^1.10.0",
|
||||
"dockerode": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.7.3",
|
||||
|
||||
@@ -10,4 +10,5 @@ spec:
|
||||
- ./create-react-app/template.yaml
|
||||
- ./springboot-grpc-template/template.yaml
|
||||
- ./v1beta2-demo/template.yaml
|
||||
- ./rails-demo/template.yaml
|
||||
- ./pull-request/template.yaml
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
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 }}'
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
gem_group :development, :test do
|
||||
gem "rspec"
|
||||
gem "rspec-rails"
|
||||
end
|
||||
|
||||
rakefile("example.rake") do
|
||||
<<-TASK
|
||||
namespace :example do
|
||||
task :backstage do
|
||||
puts "i like backstage!"
|
||||
end
|
||||
end
|
||||
TASK
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM ruby:3.0
|
||||
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
nodejs \
|
||||
postgresql-client \
|
||||
git
|
||||
|
||||
RUN gem install rails
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
} from './catalog';
|
||||
|
||||
import { createDebugLogAction } from './debug';
|
||||
import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch';
|
||||
import {
|
||||
createFetchCookiecutterAction,
|
||||
createFetchPlainAction,
|
||||
createFetchRailsAction,
|
||||
} from './fetch';
|
||||
import {
|
||||
createFilesystemDeleteAction,
|
||||
createFilesystemRenameAction,
|
||||
@@ -62,6 +66,10 @@ export const createBuiltinActions = (options: {
|
||||
integrations,
|
||||
containerRunner,
|
||||
}),
|
||||
createFetchRailsAction({
|
||||
reader,
|
||||
integrations,
|
||||
}),
|
||||
createPublishGithubAction({
|
||||
integrations,
|
||||
config,
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { createFetchPlainAction } from './plain';
|
||||
export { createFetchCookiecutterAction } from './cookiecutter';
|
||||
export { createFetchRailsAction } from './rails';
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
const mockRailsTemplater = { run: jest.fn() };
|
||||
jest.mock('./helpers');
|
||||
jest.mock('../../../stages/templater', () => {
|
||||
return {
|
||||
Rails: jest.fn().mockImplementation(() => {
|
||||
return mockRailsTemplater;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import { 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';
|
||||
|
||||
describe('fetch:rails', () => {
|
||||
const integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
azure: [
|
||||
{ host: 'dev.azure.com', token: 'tokenlols' },
|
||||
{ host: 'myazurehostnotoken.com' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const mockTmpDir = os.tmpdir();
|
||||
const mockContext = {
|
||||
input: {
|
||||
url: 'https://rubyonrails.org/generator',
|
||||
targetPath: 'something',
|
||||
values: {
|
||||
help: 'me',
|
||||
},
|
||||
},
|
||||
baseUrl: 'somebase',
|
||||
workspacePath: mockTmpDir,
|
||||
logger: getVoidLogger(),
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
|
||||
};
|
||||
|
||||
const mockReader: UrlReader = {
|
||||
read: jest.fn(),
|
||||
readTree: jest.fn(),
|
||||
search: jest.fn(),
|
||||
};
|
||||
|
||||
const action = createFetchRailsAction({
|
||||
integrations,
|
||||
reader: mockReader,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mock({ [`${mockContext.workspacePath}/result`]: {} });
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should call fetchContents with the correct values', async () => {
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(fetchContents).toHaveBeenCalledWith({
|
||||
reader: mockReader,
|
||||
integrations,
|
||||
baseUrl: mockContext.baseUrl,
|
||||
fetchUrl: mockContext.input.url,
|
||||
outputPath: resolvePath(mockContext.workspacePath),
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute the rails templater with the correct values', async () => {
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(mockRailsTemplater.run).toHaveBeenCalledWith({
|
||||
workspacePath: mockTmpDir,
|
||||
logStream: mockContext.logStream,
|
||||
values: mockContext.input.values,
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute the rails templater with optional inputs if they are present and valid', async () => {
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
imageName: 'foo/rails-custom-image',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockRailsTemplater.run).toHaveBeenCalledWith({
|
||||
workspacePath: mockTmpDir,
|
||||
logStream: mockContext.logStream,
|
||||
values: {
|
||||
...mockContext.input.values,
|
||||
imageName: 'foo/rails-custom-image',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if the target directory is outside of the workspace path', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
targetPath: '/foo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/targetPath may not specify a path outside the working directory/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DockerContainerRunner, 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 { resolve as resolvePath } from 'path';
|
||||
import { Rails, TemplaterValues } from '../../../stages/templater';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
import { fetchContents } from './helpers';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export function createFetchRailsAction(options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
}) {
|
||||
const { reader, integrations } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
url: string;
|
||||
targetPath?: string;
|
||||
values: JsonObject;
|
||||
imageName?: string;
|
||||
}>({
|
||||
id: 'fetch:rails',
|
||||
description:
|
||||
'Downloads a template from the given URL into the workspace, and runs a rails generator on it.',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['url'],
|
||||
properties: {
|
||||
url: {
|
||||
title: 'Fetch URL',
|
||||
description:
|
||||
'Relative path or absolute URL pointing to the directory tree to fetch',
|
||||
type: 'string',
|
||||
},
|
||||
targetPath: {
|
||||
title: 'Target Path',
|
||||
description:
|
||||
'Target path within the working directory to download the contents to.',
|
||||
type: 'string',
|
||||
},
|
||||
values: {
|
||||
title: 'Template Values',
|
||||
description: 'Values to pass on to rails for templating',
|
||||
type: 'object',
|
||||
properties: {
|
||||
railsArguments: {
|
||||
title: 'Arguments to pass to new command',
|
||||
description:
|
||||
'You can provide some arguments to create a custom app',
|
||||
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',
|
||||
},
|
||||
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: fromImage, dev, edge, master)',
|
||||
type: 'string',
|
||||
enum: ['dev', 'edge', 'master', 'fromImage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
imageName: {
|
||||
title: 'Rails Docker image',
|
||||
description:
|
||||
'Specify a Docker image to run rails new. Used only when a local rails is not found.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
ctx.logger.info('Fetching and then templating using rails');
|
||||
|
||||
const workDir = await ctx.createTemporaryDirectory();
|
||||
const resultDir = resolvePath(workDir, 'result');
|
||||
|
||||
await fetchContents({
|
||||
reader,
|
||||
integrations,
|
||||
baseUrl: ctx.baseUrl,
|
||||
fetchUrl: ctx.input.url,
|
||||
outputPath: workDir,
|
||||
});
|
||||
|
||||
const dockerClient = new Docker();
|
||||
const containerRunner = new DockerContainerRunner({ dockerClient });
|
||||
const templateRunner = new Rails({ containerRunner });
|
||||
|
||||
const values = {
|
||||
...(ctx.input.values as TemplaterValues),
|
||||
imageName: ctx.input.imageName,
|
||||
};
|
||||
|
||||
// Will execute the template in ./template and put the result in ./result
|
||||
await templateRunner.run({
|
||||
workspacePath: workDir,
|
||||
logStream: ctx.logStream,
|
||||
values,
|
||||
});
|
||||
|
||||
// Finally move the template result into the task workspace
|
||||
const targetPath = ctx.input.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 fs.copy(resultDir, outputPath);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2020 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 './cookiecutter';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
export * from './templaters';
|
||||
export * from './cra';
|
||||
export * from './rails';
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
const runCommand = jest.fn();
|
||||
const commandExists = jest.fn();
|
||||
|
||||
jest.mock('../helpers', () => ({ runCommand }));
|
||||
jest.mock('command-exists', () => commandExists);
|
||||
jest.mock('fs-extra');
|
||||
|
||||
import { ContainerRunner } from '@backstage/backend-common';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import path from 'path';
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
import { Rails } from './index';
|
||||
|
||||
describe('Rails Templater', () => {
|
||||
const containerRunner: jest.Mocked<ContainerRunner> = {
|
||||
runContainer: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when running on docker', () => {
|
||||
it('should run the correct bindings for the volumes', async () => {
|
||||
const values = {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
imageName: 'foo/rails-custom-image',
|
||||
};
|
||||
|
||||
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
|
||||
jest
|
||||
.spyOn(fs, 'realpath')
|
||||
.mockImplementation(x => Promise.resolve(x.toString()));
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
});
|
||||
|
||||
expect(containerRunner.runContainer).toHaveBeenCalledWith({
|
||||
imageName: 'foo/rails-custom-image',
|
||||
command: 'rails',
|
||||
args: ['new', '/output/rails-project'],
|
||||
envVars: { HOME: '/tmp' },
|
||||
mountDirs: {
|
||||
['tempdir']: '/input',
|
||||
[path.join('tempdir', 'intermediate')]: '/output',
|
||||
},
|
||||
workingDir: '/input',
|
||||
logStream: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the provided imageName', async () => {
|
||||
const values = {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
imageName: 'foo/rails-custom-image',
|
||||
};
|
||||
|
||||
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
});
|
||||
|
||||
expect(containerRunner.runContainer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
imageName: 'foo/rails-custom-image',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass through the streamer to the run docker helper', async () => {
|
||||
const stream = new PassThrough();
|
||||
|
||||
const values = {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
imageName: 'foo/rails-custom-image',
|
||||
};
|
||||
|
||||
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
logStream: stream,
|
||||
});
|
||||
|
||||
expect(containerRunner.runContainer).toHaveBeenCalledWith({
|
||||
imageName: 'foo/rails-custom-image',
|
||||
command: 'rails',
|
||||
args: ['new', '/output/rails-project'],
|
||||
envVars: { HOME: '/tmp' },
|
||||
mountDirs: {
|
||||
['tempdir']: '/input',
|
||||
[path.join('tempdir', 'intermediate')]: '/output',
|
||||
},
|
||||
workingDir: '/input',
|
||||
logStream: stream,
|
||||
});
|
||||
});
|
||||
|
||||
it('update the template path to correct location', async () => {
|
||||
const values = {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
railsArguments: { template: './something.rb' },
|
||||
imageName: 'foo/rails-custom-image',
|
||||
};
|
||||
|
||||
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
|
||||
jest
|
||||
.spyOn(fs, 'realpath')
|
||||
.mockImplementation(x => Promise.resolve(x.toString()));
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
});
|
||||
|
||||
expect(containerRunner.runContainer).toHaveBeenCalledWith({
|
||||
imageName: 'foo/rails-custom-image',
|
||||
command: 'rails',
|
||||
args: [
|
||||
'new',
|
||||
'/output/rails-project',
|
||||
'--template',
|
||||
'/input/something.rb',
|
||||
],
|
||||
envVars: { HOME: '/tmp' },
|
||||
mountDirs: {
|
||||
['tempdir']: '/input',
|
||||
[path.join('tempdir', 'intermediate')]: '/output',
|
||||
},
|
||||
workingDir: '/input',
|
||||
logStream: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rails is available', () => {
|
||||
it('use the binary', async () => {
|
||||
const stream = new PassThrough();
|
||||
|
||||
const values = {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
imageName: 'foo/rails-custom-image',
|
||||
};
|
||||
|
||||
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
|
||||
commandExists.mockImplementationOnce(() => () => true);
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
logStream: stream,
|
||||
});
|
||||
|
||||
expect(runCommand).toHaveBeenCalledWith({
|
||||
command: 'rails',
|
||||
args: expect.arrayContaining([
|
||||
'new',
|
||||
path.join('tempdir', 'intermediate', 'rails-project'),
|
||||
]),
|
||||
logStream: stream,
|
||||
});
|
||||
});
|
||||
it('update the template path to correct location', async () => {
|
||||
const stream = new PassThrough();
|
||||
|
||||
const values = {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
railsArguments: { template: './something.rb' },
|
||||
imageName: 'foo/rails-custom-image',
|
||||
};
|
||||
|
||||
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
|
||||
commandExists.mockImplementationOnce(() => () => true);
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
logStream: stream,
|
||||
});
|
||||
|
||||
expect(runCommand).toHaveBeenCalledWith({
|
||||
command: 'rails',
|
||||
args: expect.arrayContaining([
|
||||
'new',
|
||||
path.join('tempdir', 'intermediate', 'rails-project'),
|
||||
'--template',
|
||||
path.join('tempdir', './something.rb'),
|
||||
]),
|
||||
logStream: stream,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when nothing was generated', () => {
|
||||
it('throws an error', async () => {
|
||||
const stream = new PassThrough();
|
||||
|
||||
jest
|
||||
.spyOn(fs, 'readdir')
|
||||
.mockImplementationOnce(() => Promise.resolve([]));
|
||||
|
||||
const templater = new Rails({ containerRunner });
|
||||
await expect(
|
||||
templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values: {
|
||||
owner: 'angeliski',
|
||||
storePath: 'https://github.com/angeliski/rails-project',
|
||||
name: 'rails-project',
|
||||
imageName: 'foo/rails-custom-image',
|
||||
},
|
||||
logStream: stream,
|
||||
}),
|
||||
).rejects.toThrow(/No data generated by rails/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2021 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 { ContainerRunner } from '@backstage/backend-common';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { runCommand } from '../helpers';
|
||||
import commandExists from 'command-exists';
|
||||
import { TemplaterBase, TemplaterRunOptions } from '../types';
|
||||
import { railsArgumentResolver } from './railsArgumentResolver';
|
||||
|
||||
export class Rails implements TemplaterBase {
|
||||
private readonly containerRunner: ContainerRunner;
|
||||
|
||||
constructor({ containerRunner }: { containerRunner: ContainerRunner }) {
|
||||
this.containerRunner = containerRunner;
|
||||
}
|
||||
|
||||
public async run({
|
||||
workspacePath,
|
||||
values,
|
||||
logStream,
|
||||
}: TemplaterRunOptions): Promise<void> {
|
||||
const intermediateDir = path.join(workspacePath, 'intermediate');
|
||||
await fs.ensureDir(intermediateDir);
|
||||
const resultDir = path.join(workspacePath, 'result');
|
||||
|
||||
const { name, imageName, railsArguments } = values;
|
||||
|
||||
// Directories to bind on container
|
||||
const mountDirs = {
|
||||
[workspacePath]: '/input',
|
||||
[intermediateDir]: '/output',
|
||||
};
|
||||
|
||||
const baseCommand = 'rails';
|
||||
const baseArguments = ['new'];
|
||||
const commandExistsToRun = await commandExists(baseCommand);
|
||||
|
||||
if (commandExistsToRun) {
|
||||
const arrayExtraArguments = railsArgumentResolver(
|
||||
workspacePath,
|
||||
railsArguments,
|
||||
);
|
||||
|
||||
await runCommand({
|
||||
command: baseCommand,
|
||||
args: [
|
||||
...baseArguments,
|
||||
`${intermediateDir}/${name}`,
|
||||
...arrayExtraArguments,
|
||||
],
|
||||
logStream,
|
||||
});
|
||||
} else {
|
||||
const arrayExtraArguments = railsArgumentResolver(
|
||||
'/input',
|
||||
railsArguments,
|
||||
);
|
||||
await this.containerRunner.runContainer({
|
||||
imageName: imageName,
|
||||
command: baseCommand,
|
||||
args: [...baseArguments, `/output/${name}`, ...arrayExtraArguments],
|
||||
mountDirs,
|
||||
workingDir: '/input',
|
||||
// Set the home directory inside the container as something that applications can
|
||||
// write to, otherwise they will just fail trying to write to /
|
||||
envVars: { HOME: '/tmp' },
|
||||
logStream,
|
||||
});
|
||||
}
|
||||
|
||||
// if command was successful, intermediateDir should contain
|
||||
// exactly one directory.
|
||||
const [generated] = await fs.readdir(intermediateDir);
|
||||
|
||||
if (generated === undefined) {
|
||||
throw new Error(`No data generated by ${baseCommand}`);
|
||||
}
|
||||
|
||||
await fs.move(path.join(intermediateDir, generated), resultDir);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2021 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 { railsArgumentResolver } from './railsArgumentResolver';
|
||||
import { TemplaterValues } from '../types';
|
||||
|
||||
describe('railsArgumentResolver', () => {
|
||||
describe('when provide the parameter', () => {
|
||||
test.each([
|
||||
[{}, []],
|
||||
[{ minimal: true }, ['--minimal']],
|
||||
[{ api: true }, ['--api']],
|
||||
[{ skipBundle: true }, ['--skip-bundle']],
|
||||
[{ skipWebpackInstall: true }, ['--skip-webpack-install']],
|
||||
[{ webpacker: 'vue' }, ['--webpack', 'vue']],
|
||||
[{ database: 'postgresql' }, ['--database', 'postgresql']],
|
||||
[{ railsVersion: 'dev' }, ['--dev']],
|
||||
[{ template: './rails.rb' }, ['--template', '/tmp/rails.rb']],
|
||||
])(
|
||||
'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 = {
|
||||
owner: 'r',
|
||||
storePath: '',
|
||||
railsArguments: passedArguments,
|
||||
};
|
||||
|
||||
const { railsArguments } = values;
|
||||
|
||||
const argumentsToRun = railsArgumentResolver('/tmp', railsArguments);
|
||||
|
||||
expect(argumentsToRun).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
enum Webpacker {
|
||||
react = 'react',
|
||||
vue = 'vue',
|
||||
angular = 'angular',
|
||||
elm = 'elm',
|
||||
stimulus = 'stimulus',
|
||||
}
|
||||
|
||||
enum Database {
|
||||
mysql = 'mysql',
|
||||
postgresql = 'postgresql',
|
||||
sqlite3 = 'sqlite3',
|
||||
oracle = 'oracle',
|
||||
sqlserver = 'sqlserver',
|
||||
jdbcmysql = 'jdbcmysql',
|
||||
jdbcsqlite3 = 'jdbcsqlite3',
|
||||
jdbcpostgresql = 'jdbcpostgresql',
|
||||
jdbc = 'jdbc',
|
||||
}
|
||||
|
||||
enum RailsVersion {
|
||||
dev = 'dev',
|
||||
edge = 'edge',
|
||||
master = 'master',
|
||||
fromImage = 'fromImage',
|
||||
}
|
||||
|
||||
export type RailsRunOptions = {
|
||||
minimal?: boolean;
|
||||
api?: boolean;
|
||||
template?: string;
|
||||
webpacker?: Webpacker;
|
||||
database?: Database;
|
||||
railsVersion?: RailsVersion;
|
||||
skipBundle?: boolean;
|
||||
skipWebpackInstall?: boolean;
|
||||
};
|
||||
|
||||
export const railsArgumentResolver = (
|
||||
projectRoot: string,
|
||||
options: RailsRunOptions,
|
||||
): string[] => {
|
||||
const argumentsToRun: string[] = [];
|
||||
|
||||
if (options?.minimal) {
|
||||
argumentsToRun.push('--minimal');
|
||||
}
|
||||
|
||||
if (options?.api) {
|
||||
argumentsToRun.push('--api');
|
||||
}
|
||||
|
||||
if (options?.skipBundle) {
|
||||
argumentsToRun.push('--skip-bundle');
|
||||
}
|
||||
|
||||
if (options?.skipWebpackInstall) {
|
||||
argumentsToRun.push('--skip-webpack-install');
|
||||
}
|
||||
|
||||
if (
|
||||
options?.webpacker &&
|
||||
Object.values(Webpacker).includes(options?.webpacker as Webpacker)
|
||||
) {
|
||||
argumentsToRun.push('--webpack');
|
||||
argumentsToRun.push(options.webpacker);
|
||||
}
|
||||
|
||||
if (
|
||||
options?.database &&
|
||||
Object.values(Database).includes(options?.database as Database)
|
||||
) {
|
||||
argumentsToRun.push('--database');
|
||||
argumentsToRun.push(options.database);
|
||||
}
|
||||
|
||||
if (
|
||||
options?.railsVersion !== RailsVersion.fromImage &&
|
||||
Object.values(RailsVersion).includes(options?.railsVersion as RailsVersion)
|
||||
) {
|
||||
argumentsToRun.push(`--${options.railsVersion}`);
|
||||
}
|
||||
|
||||
if (options?.template) {
|
||||
argumentsToRun.push('--template');
|
||||
argumentsToRun.push(options.template.replace('./', `${projectRoot}/`));
|
||||
}
|
||||
|
||||
return argumentsToRun;
|
||||
};
|
||||
Reference in New Issue
Block a user