Merge pull request #6016 from angeliski/rails-generate
feat: added rails templater to scaffolder-backend
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
@@ -18,7 +18,7 @@ export interface RouterOptions {
|
||||
config: Config;
|
||||
disableConfigInjection?: boolean;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
staticFallbackHandler?: express.Handler;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Config } from '@backstage/config';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import { JSONWebKey } from 'jose';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { Profile } from 'passport';
|
||||
@@ -23,7 +23,7 @@ export type AuthProviderFactoryOptions = {
|
||||
providerId: string;
|
||||
globalConfig: AuthProviderConfig;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
tokenIssuer: TokenIssuer;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
catalogApi: CatalogApi;
|
||||
@@ -206,7 +206,7 @@ export interface RouterOptions {
|
||||
// (undocumented)
|
||||
discovery: PluginEndpointDiscovery;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
// (undocumented)
|
||||
providerFactories?: ProviderFactories;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Config } from '@backstage/config';
|
||||
import { GroupEntity } from '@backstage/catalog-model';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { SearchEntry } from 'ldapjs';
|
||||
import { SearchOptions } from 'ldapjs';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
@@ -38,7 +38,7 @@ export const LDAP_UUID_ANNOTATION = "backstage.io/ldap-uuid";
|
||||
export class LdapClient {
|
||||
constructor(client: Client);
|
||||
// (undocumented)
|
||||
static create(logger: Logger, target: string, bind?: BindConfig): Promise<LdapClient>;
|
||||
static create(logger: Logger_2, target: string, bind?: BindConfig): Promise<LdapClient>;
|
||||
getRootDSE(): Promise<SearchEntry | undefined>;
|
||||
getVendor(): Promise<LdapVendor>;
|
||||
search(dn: string, options: SearchOptions): Promise<SearchEntry[]>;
|
||||
@@ -48,13 +48,13 @@ export class LdapClient {
|
||||
export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
providers: LdapProviderConfig[];
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
groupTransformer?: GroupTransformer;
|
||||
userTransformer?: UserTransformer;
|
||||
});
|
||||
// (undocumented)
|
||||
static fromConfig(config: Config, options: {
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
groupTransformer?: GroupTransformer;
|
||||
userTransformer?: UserTransformer;
|
||||
}): LdapOrgReaderProcessor;
|
||||
@@ -80,7 +80,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[];
|
||||
export function readLdapOrg(client: LdapClient, userConfig: UserConfig, groupConfig: GroupConfig, options: {
|
||||
groupTransformer?: GroupTransformer;
|
||||
userTransformer?: UserTransformer;
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
}): Promise<{
|
||||
users: UserEntity[];
|
||||
groups: GroupEntity[];
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
|
||||
import { Config } from '@backstage/config';
|
||||
import { GroupEntity } from '@backstage/catalog-model';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import * as msal from '@azure/msal-node';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
@@ -70,12 +70,12 @@ export class MicrosoftGraphClient {
|
||||
export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
providers: MicrosoftGraphProviderConfig[];
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
groupTransformer?: GroupTransformer;
|
||||
});
|
||||
// (undocumented)
|
||||
static fromConfig(config: Config, options: {
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
groupTransformer?: GroupTransformer;
|
||||
}): MicrosoftGraphOrgReaderProcessor;
|
||||
// (undocumented)
|
||||
@@ -107,7 +107,7 @@ export function readMicrosoftGraphOrg(client: MicrosoftGraphClient, tenantId: st
|
||||
userFilter?: string;
|
||||
groupFilter?: string;
|
||||
groupTransformer?: GroupTransformer;
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
}): Promise<{
|
||||
users: UserEntity[];
|
||||
groups: GroupEntity[];
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { GraphQLModule } from '@graphql-modules/core';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createModule(options: ModuleOptions): Promise<GraphQLModule>;
|
||||
@@ -16,7 +16,7 @@ export interface ModuleOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
@@ -32,7 +32,7 @@ export interface RouterOptions {
|
||||
// (undocumented)
|
||||
discovery: PluginEndpointDiscovery;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
// (undocumented)
|
||||
urlReader: UrlReader;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
@@ -16,7 +16,7 @@ export interface RouterOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
@@ -9,7 +9,7 @@ import express from 'express';
|
||||
import { FetchResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ClusterDetails {
|
||||
@@ -68,7 +68,7 @@ export interface KubernetesServiceLocator {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const makeRouter: (logger: Logger, kubernetesFanOutHandler: KubernetesFanOutHandler, clusterDetails: ClusterDetails[]) => express.Router;
|
||||
export const makeRouter: (logger: Logger_2, kubernetesFanOutHandler: KubernetesFanOutHandler, clusterDetails: ClusterDetails[]) => express.Router;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ObjectFetchParams {
|
||||
@@ -91,7 +91,7 @@ export interface RouterOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
@@ -20,7 +20,7 @@ export function getRequestHeaders(token: string): {
|
||||
|
||||
// @public (undocumented)
|
||||
export class RollbarApi {
|
||||
constructor(accessToken: string, logger: Logger);
|
||||
constructor(accessToken: string, logger: Logger_2);
|
||||
// (undocumented)
|
||||
getActivatedCounts(projectName: string, options?: {
|
||||
environment: string;
|
||||
@@ -49,7 +49,7 @@ export interface RouterOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
// (undocumented)
|
||||
rollbarApi?: RollbarApi;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,247 @@
|
||||
# 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:
|
||||
|
||||
## From your Backstage root directory
|
||||
|
||||
```
|
||||
cd packages/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({
|
||||
containerRunner,
|
||||
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 }}'
|
||||
```
|
||||
|
||||
### What you need to run that action
|
||||
|
||||
The environment need to have a [rails](https://github.com/rails/rails#getting-started) installation, or you can build and provide a docker image in your template.
|
||||
|
||||
We have a [Dockerfile](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/Rails.dockerfile) that you can use to build your image.
|
||||
|
||||
If you choose to provide a docker image, you need to update your template with `imageName` parameter:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- id: fetch-base
|
||||
name: Fetch Base
|
||||
action: fetch:rails
|
||||
input:
|
||||
url: ./template
|
||||
imageName: repository/rails:tag
|
||||
values:
|
||||
name: '{{ parameters.name }}'
|
||||
owner: '{{ parameters.owner }}'
|
||||
system: '{{ parameters.system }}'
|
||||
railsArguments: '{{ json parameters.railsArguments }}'
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM ruby:3.0
|
||||
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y nodejs postgresql-client git && \
|
||||
rm -rf /var/lib/apt/lists/
|
||||
|
||||
RUN gem install rails
|
||||
@@ -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",
|
||||
"private": false,
|
||||
"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",
|
||||
"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';
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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('@backstage/plugin-scaffolder-backend', () => ({
|
||||
...jest.requireActual('@backstage/plugin-scaffolder-backend'),
|
||||
fetchContents: jest.fn(),
|
||||
}));
|
||||
jest.mock('./railsNewRunner', () => {
|
||||
return {
|
||||
RailsNewRunner: jest.fn().mockImplementation(() => {
|
||||
return mockRailsTemplater;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
ContainerRunner,
|
||||
getVoidLogger,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { PassThrough } from 'stream';
|
||||
import { createFetchRailsAction } from './index';
|
||||
import { fetchContents } from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
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 containerRunner: ContainerRunner = {
|
||||
runContainer: jest.fn(),
|
||||
};
|
||||
|
||||
const action = createFetchRailsAction({
|
||||
integrations,
|
||||
reader: mockReader,
|
||||
containerRunner,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({ [`${mockContext.workspacePath}/result`]: {} });
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.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,183 @@
|
||||
/*
|
||||
* 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, 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 { RailsNewRunner } from './railsNewRunner';
|
||||
|
||||
export function createFetchRailsAction(options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
containerRunner: ContainerRunner;
|
||||
}) {
|
||||
const { reader, integrations, containerRunner } = 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 new 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 templateRunner = new RailsNewRunner({ containerRunner });
|
||||
|
||||
const values = {
|
||||
...ctx.input.values,
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { sep as separatorPath } from 'path';
|
||||
import os from 'os';
|
||||
|
||||
describe('railsArgumentResolver', () => {
|
||||
describe('when provide the parameter', () => {
|
||||
const root = os.platform() === 'win32' ? 'C:\\' : '/';
|
||||
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: `.${separatorPath}rails.rb` },
|
||||
['--template', `${root}${separatorPath}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 = {
|
||||
owner: 'r',
|
||||
storePath: '',
|
||||
railsArguments: passedArguments,
|
||||
};
|
||||
|
||||
const { railsArguments } = values;
|
||||
|
||||
const argumentsToRun = railsArgumentResolver(root, railsArguments);
|
||||
|
||||
expect(argumentsToRun).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 { sep as separatorPath } from 'path';
|
||||
|
||||
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(
|
||||
`.${separatorPath}`,
|
||||
`${projectRoot}${separatorPath}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return argumentsToRun;
|
||||
};
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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('@backstage/plugin-scaffolder-backend', () => ({ 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 { RailsNewRunner } from './railsNewRunner';
|
||||
|
||||
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 logStream = 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);
|
||||
jest
|
||||
.spyOn(fs, 'realpath')
|
||||
.mockImplementation(x => Promise.resolve(x.toString()));
|
||||
|
||||
const templater = new RailsNewRunner({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
logStream,
|
||||
});
|
||||
|
||||
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: logStream,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the provided imageName', async () => {
|
||||
const logStream = 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 RailsNewRunner({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
logStream,
|
||||
});
|
||||
|
||||
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 RailsNewRunner({ 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 logStream = 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);
|
||||
jest
|
||||
.spyOn(fs, 'realpath')
|
||||
.mockImplementation(x => Promise.resolve(x.toString()));
|
||||
|
||||
const templater = new RailsNewRunner({ containerRunner });
|
||||
await templater.run({
|
||||
workspacePath: 'tempdir',
|
||||
values,
|
||||
logStream,
|
||||
});
|
||||
|
||||
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: logStream,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 RailsNewRunner({ 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 RailsNewRunner({ 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 RailsNewRunner({ 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,104 @@
|
||||
/*
|
||||
* 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 '@backstage/plugin-scaffolder-backend';
|
||||
import commandExists from 'command-exists';
|
||||
import {
|
||||
railsArgumentResolver,
|
||||
RailsRunOptions,
|
||||
} from './railsArgumentResolver';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
export class RailsNewRunner {
|
||||
private readonly containerRunner: ContainerRunner;
|
||||
|
||||
constructor({ containerRunner }: { containerRunner: ContainerRunner }) {
|
||||
this.containerRunner = containerRunner;
|
||||
}
|
||||
|
||||
public async run({
|
||||
workspacePath,
|
||||
values,
|
||||
logStream,
|
||||
}: {
|
||||
workspacePath: string;
|
||||
values: JsonObject;
|
||||
logStream: Writable;
|
||||
}): 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 as RailsRunOptions,
|
||||
);
|
||||
|
||||
await runCommand({
|
||||
command: baseCommand,
|
||||
args: [
|
||||
...baseArguments,
|
||||
`${intermediateDir}/${name}`,
|
||||
...arrayExtraArguments,
|
||||
],
|
||||
logStream,
|
||||
});
|
||||
} else {
|
||||
const arrayExtraArguments = railsArgumentResolver(
|
||||
'/input',
|
||||
railsArguments as RailsRunOptions,
|
||||
);
|
||||
await this.containerRunner.runContainer({
|
||||
imageName: imageName as string,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -118,6 +118,15 @@ export const createTemplateAction: <Input extends Partial<{
|
||||
[name: string]: JsonValue| Partial<JsonObject> | undefined;
|
||||
}>>(templateAction: TemplateAction<Input>) => TemplateAction<any>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function fetchContents({ reader, integrations, baseUrl, fetchUrl, outputPath, }: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
baseUrl?: string;
|
||||
fetchUrl?: JsonValue;
|
||||
outputPath: string;
|
||||
}): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
@@ -131,13 +140,16 @@ export interface RouterOptions {
|
||||
// (undocumented)
|
||||
database: PluginDatabaseManager;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
// (undocumented)
|
||||
reader: UrlReader;
|
||||
// (undocumented)
|
||||
taskWorkers?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const runCommand: ({ command, args, logStream, }: RunCommandOptions) => Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TemplateAction<Input extends InputBase> = {
|
||||
id: string;
|
||||
|
||||
@@ -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
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { createFetchPlainAction } from './plain';
|
||||
export { createFetchCookiecutterAction } from './cookiecutter';
|
||||
export { fetchContents } from './helpers';
|
||||
|
||||
@@ -20,3 +20,4 @@ export * from './debug';
|
||||
export * from './fetch';
|
||||
export * from './filesystem';
|
||||
export * from './publish';
|
||||
export { runCommand } from './helpers';
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { DocumentCollator } from '@backstage/search-common';
|
||||
import { DocumentDecorator } from '@backstage/search-common';
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { default as lunr_2 } from 'lunr';
|
||||
import { SearchQuery } from '@backstage/search-common';
|
||||
import { SearchResultSet } from '@backstage/search-common';
|
||||
@@ -27,14 +27,14 @@ export class IndexBuilder {
|
||||
// @public (undocumented)
|
||||
export class LunrSearchEngine implements SearchEngine {
|
||||
constructor({ logger }: {
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
});
|
||||
// (undocumented)
|
||||
protected docStore: Record<string, IndexableDocument>;
|
||||
// (undocumented)
|
||||
index(type: string, documents: IndexableDocument[]): void;
|
||||
// (undocumented)
|
||||
protected logger: Logger;
|
||||
protected logger: Logger_2;
|
||||
// (undocumented)
|
||||
protected lunrIndices: Record<string, lunr_2.Index>;
|
||||
// (undocumented)
|
||||
@@ -48,7 +48,7 @@ export class LunrSearchEngine implements SearchEngine {
|
||||
// @public
|
||||
export class Scheduler {
|
||||
constructor({ logger }: {
|
||||
logger: Logger;
|
||||
logger: Logger_2;
|
||||
});
|
||||
addToSchedule(task: Function, interval: number): void;
|
||||
start(): void;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
```ts
|
||||
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { SearchEngine } from '@backstage/plugin-search-backend-node';
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { GeneratorBuilder } from '@backstage/techdocs-common';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { PreparerBuilder } from '@backstage/techdocs-common';
|
||||
import { PublisherBase } from '@backstage/techdocs-common';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user