diff --git a/.changeset/twelve-deers-explode.md b/.changeset/twelve-deers-explode.md new file mode 100644 index 0000000000..0d34392eab --- /dev/null +++ b/.changeset/twelve-deers-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Export the `fetchContents` from scaffolder-backend diff --git a/microsite/data/plugins/scaffolder-backend-module-rails.yaml b/microsite/data/plugins/scaffolder-backend-module-rails.yaml new file mode 100644 index 0000000000..5f5a29cf3c --- /dev/null +++ b/microsite/data/plugins/scaffolder-backend-module-rails.yaml @@ -0,0 +1,9 @@ +--- +title: Scaffolder Backend Module Rails +author: Rogerio Angeliski +authorUrl: https://angeliski.com.br/ +category: Scaffolder +description: Here you can find all Rails related features to improve your scaffolder. +documentation: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/README.md +iconUrl: img/rails-icon.png +npmPackageName: '@backstage/plugin-scaffolder-backend-module-rails' diff --git a/microsite/static/img/rails-icon.png b/microsite/static/img/rails-icon.png new file mode 100644 index 0000000000..f7b8b69bd9 Binary files /dev/null and b/microsite/static/img/rails-icon.png differ diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index a21aaef4a6..22eac06ed2 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -19,7 +19,7 @@ import * as http from 'http'; import { isChildPath } from '@backstage/cli-common'; import { JsonValue } from '@backstage/config'; import { Knex } from 'knex'; -import { Logger } from 'winston'; +import { Logger as Logger_2 } from 'winston'; import { MergeResult } from 'isomorphic-git'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; @@ -129,7 +129,7 @@ export function errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler // @public (undocumented) export type ErrorHandlerOptions = { showStackTraces?: boolean; - logger?: Logger; + logger?: Logger_2; logClientErrors?: boolean; }; @@ -185,7 +185,7 @@ export class Git { static fromAuth: ({ username, password, logger, }: { username?: string | undefined; password?: string | undefined; - logger?: Logger | undefined; + logger?: Logger_2 | undefined; }) => Git; // (undocumented) init({ dir, defaultBranch, }: { @@ -301,7 +301,7 @@ export type ReadTreeResponseFile = { }; // @public -export function requestLoggingHandler(logger?: Logger): RequestHandler; +export function requestLoggingHandler(logger?: Logger_2): RequestHandler; // @public export function resolvePackagePath(name: string, ...paths: string[]): string; @@ -337,7 +337,7 @@ export type ServiceBuilder = { loadConfig(config: ConfigReader): ServiceBuilder; setPort(port: number): ServiceBuilder; setHost(host: string): ServiceBuilder; - setLogger(logger: Logger): ServiceBuilder; + setLogger(logger: Logger_2): ServiceBuilder; enableCors(options: cors.CorsOptions): ServiceBuilder; setHttpsSettings(settings: HttpsSettings): ServiceBuilder; addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; diff --git a/packages/backend/package.json b/packages/backend/package.json index ed6fbd56b5..c3e693865f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -31,6 +31,7 @@ "@backstage/catalog-client": "^0.3.15", "@backstage/catalog-model": "^0.8.2", "@backstage/config": "^0.1.5", + "@backstage/integration": "^0.5.6", "@backstage/plugin-app-backend": "^0.3.13", "@backstage/plugin-auth-backend": "^0.3.15", "@backstage/plugin-badges-backend": "^0.1.6", @@ -42,6 +43,7 @@ "@backstage/plugin-proxy-backend": "^0.2.9", "@backstage/plugin-rollbar-backend": "^0.1.11", "@backstage/plugin-scaffolder-backend": "^0.12.4", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.1", "@backstage/plugin-search-backend": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.2.0", "@backstage/plugin-techdocs-backend": "^0.8.5", diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 3fff030eee..d25f55bf3a 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -12,17 +12,17 @@ import { EntityName } from '@backstage/catalog-model'; import express from 'express'; import { GitHubIntegrationConfig } from '@backstage/integration'; import { GitLabIntegrationConfig } from '@backstage/integration'; -import { Logger } from 'winston'; +import { Logger as Logger_2 } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; // @public (undocumented) -export const checkoutGitRepository: (repoUrl: string, config: Config, logger: Logger) => Promise; +export const checkoutGitRepository: (repoUrl: string, config: Config, logger: Logger_2) => Promise; // @public (undocumented) export class CommonGitPreparer implements PreparerBase { - constructor(config: Config, logger: Logger); + constructor(config: Config, logger: Logger_2); // (undocumented) prepare(entity: Entity, options?: { etag?: string; @@ -31,7 +31,7 @@ export class CommonGitPreparer implements PreparerBase { // @public (undocumented) export class DirectoryPreparer implements PreparerBase { - constructor(config: Config, logger: Logger, reader: UrlReader); + constructor(config: Config, logger: Logger_2, reader: UrlReader); // (undocumented) prepare(entity: Entity): Promise; } @@ -51,7 +51,7 @@ export type GeneratorBuilder = { export class Generators implements GeneratorBuilder { // (undocumented) static fromConfig(config: Config, { logger, containerRunner, }: { - logger: Logger; + logger: Logger_2; containerRunner: ContainerRunner; }): Promise; // (undocumented) @@ -69,7 +69,7 @@ export const getDefaultBranch: (repositoryUrl: string, config: Config) => Promis // @public (undocumented) export const getDocFilesFromRepository: (reader: UrlReader, entity: Entity, opts?: { etag?: string | undefined; - logger?: Logger | undefined; + logger?: Logger_2 | undefined; } | undefined) => Promise; // @public (undocumented) @@ -88,7 +88,7 @@ export const getGitRepositoryTempFolder: (repositoryUrl: string, config: Config) export function getGitRepoType(url: string): string; // @public (undocumented) -export const getLastCommitTimestamp: (repositoryLocation: string, logger: Logger) => Promise; +export const getLastCommitTimestamp: (repositoryLocation: string, logger: Logger_2) => Promise; // @public (undocumented) export const getLocationForEntity: (entity: Entity) => ParsedLocationAnnotation; @@ -108,7 +108,7 @@ export const parseReferenceAnnotation: (annotationName: string, entity: Entity) // @public (undocumented) export type PreparerBase = { prepare(entity: Entity, options?: { - logger?: Logger; + logger?: Logger_2; etag?: string; }): Promise; }; @@ -153,7 +153,7 @@ export type RemoteProtocol = 'url' | 'dir' | 'github' | 'gitlab' | 'file' | 'azu // @public (undocumented) export class TechdocsGenerator implements GeneratorBase { constructor({ logger, containerRunner, config, }: { - logger: Logger; + logger: Logger_2; containerRunner: ContainerRunner; config: Config; }); @@ -170,7 +170,7 @@ export type TechDocsMetadata = { // @public (undocumented) export class UrlPreparer implements PreparerBase { - constructor(reader: UrlReader, logger: Logger); + constructor(reader: UrlReader, logger: Logger_2); // (undocumented) prepare(entity: Entity, options?: { etag?: string; diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index 4f59102265..93aba5596f 100644 --- a/plugins/app-backend/api-report.md +++ b/plugins/app-backend/api-report.md @@ -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; @@ -18,7 +18,7 @@ export interface RouterOptions { config: Config; disableConfigInjection?: boolean; // (undocumented) - logger: Logger; + logger: Logger_2; staticFallbackHandler?: express.Handler; } diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 6ae878accf..9f9a8d18ef 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -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; } diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 55405cab41..446dc2df52 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -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; + static create(logger: Logger_2, target: string, bind?: BindConfig): Promise; getRootDSE(): Promise; getVendor(): Promise; search(dn: string, options: SearchOptions): Promise; @@ -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[]; diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index f1e7e22c35..c755514b48 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -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[]; diff --git a/plugins/catalog-graphql/api-report.md b/plugins/catalog-graphql/api-report.md index cbe325f4c5..40e8a19b66 100644 --- a/plugins/catalog-graphql/api-report.md +++ b/plugins/catalog-graphql/api-report.md @@ -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; @@ -16,7 +16,7 @@ export interface ModuleOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger; + logger: Logger_2; } diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index f8f1575d0b..186b76fb39 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -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; } diff --git a/plugins/graphql/api-report.md b/plugins/graphql/api-report.md index 8109c94122..ef172afbd7 100644 --- a/plugins/graphql/api-report.md +++ b/plugins/graphql/api-report.md @@ -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; @@ -16,7 +16,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger; + logger: Logger_2; } diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index 47754ad572..6840bd7527 100644 --- a/plugins/kafka-backend/api-report.md +++ b/plugins/kafka-backend/api-report.md @@ -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; diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index fe43e8888a..a9389b7e62 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -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) diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index 1b1eba3b00..63469a2017 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -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) diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md index fa8c67d4f0..9fa525cfc3 100644 --- a/plugins/rollbar-backend/api-report.md +++ b/plugins/rollbar-backend/api-report.md @@ -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; @@ -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; } diff --git a/plugins/scaffolder-backend-module-rails/.eslintrc.js b/plugins/scaffolder-backend-module-rails/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/scaffolder-backend-module-rails/README.md b/plugins/scaffolder-backend-module-rails/README.md new file mode 100644 index 0000000000..4ac52be941 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -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 }}' +``` diff --git a/plugins/scaffolder-backend-module-rails/Rails.dockerfile b/plugins/scaffolder-backend-module-rails/Rails.dockerfile new file mode 100644 index 0000000000..0774fdc975 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/Rails.dockerfile @@ -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 diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md new file mode 100644 index 0000000000..95d9c1bd9f --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -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; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json new file mode 100644 index 0000000000..b513100f16 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -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" + ] +} diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/index.ts new file mode 100644 index 0000000000..7d590c9a1a --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/index.ts @@ -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'; diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts new file mode 100644 index 0000000000..6b98dd86c6 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -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/, + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts new file mode 100644 index 0000000000..e2dc8fd95a --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -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); + }, + }); +} diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts new file mode 100644 index 0000000000..99a113f4f7 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts @@ -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) => { + // 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); + }, + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts new file mode 100644 index 0000000000..b1ddadc6d9 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts @@ -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; +}; diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts new file mode 100644 index 0000000000..dc3f824236 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts @@ -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 = { + 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/); + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts new file mode 100644 index 0000000000..3e18490587 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts @@ -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 { + 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); + } +} diff --git a/plugins/scaffolder-backend-module-rails/src/actions/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/index.ts new file mode 100644 index 0000000000..1b47db9e03 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/index.ts @@ -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'; diff --git a/plugins/scaffolder-backend-module-rails/src/index.ts b/plugins/scaffolder-backend-module-rails/src/index.ts new file mode 100644 index 0000000000..4f06b14a86 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/index.ts @@ -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'; diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index ab3ea433c6..e8e8697291 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -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 = { baseUrl?: string; - logger: Logger; + logger: Logger_2; logStream: Writable; token?: string | undefined; workspacePath: string; @@ -118,6 +118,15 @@ export const createTemplateAction: | undefined; }>>(templateAction: TemplateAction) => TemplateAction; +// @public (undocumented) +export function fetchContents({ reader, integrations, baseUrl, fetchUrl, outputPath, }: { + reader: UrlReader; + integrations: ScmIntegrations; + baseUrl?: string; + fetchUrl?: JsonValue; + outputPath: string; +}): Promise; + // @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; + // @public (undocumented) export type TemplateAction = { id: string; diff --git a/plugins/scaffolder-backend/sample-templates/rails-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/rails-demo/template.yaml new file mode 100644 index 0000000000..174c58741b --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/rails-demo/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 }}' diff --git a/plugins/scaffolder-backend/sample-templates/rails-demo/template/rails-template-file.rb b/plugins/scaffolder-backend/sample-templates/rails-demo/template/rails-template-file.rb new file mode 100644 index 0000000000..90c0d53ddc --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/rails-demo/template/rails-template-file.rb @@ -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 diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts index 8e0f93f3ab..fa6d23c032 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts @@ -16,3 +16,4 @@ export { createFetchPlainAction } from './plain'; export { createFetchCookiecutterAction } from './cookiecutter'; +export { fetchContents } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index a5d5092327..849b056ac9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -20,3 +20,4 @@ export * from './debug'; export * from './fetch'; export * from './filesystem'; export * from './publish'; +export { runCommand } from './helpers'; diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 03585b6b99..ccb7475573 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -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; // (undocumented) index(type: string, documents: IndexableDocument[]): void; // (undocumented) - protected logger: Logger; + protected logger: Logger_2; // (undocumented) protected lunrIndices: Record; // (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; diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index 9e617b73ed..7eba677439 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -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) diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index d433d428e0..947439533a 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -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'; diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index 8bcfe30aee..d203a52b36 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -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';